diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index cdf6ce5f5..51bf3a451 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -48,8 +48,13 @@ namespace Barotrauma { color = Color.CornflowerBlue; } - else if (Entity is Item) + else if (Entity is Item i) { + if (i.Submarine != null && i.GetComponent() == null) + { + // Don't show items that are inside the submarine, because monsters shouldn't target them when they are inside and the monsters are outside. + return; + } color = Color.CadetBlue; } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index e01494501..74548b53d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -81,10 +81,10 @@ namespace Barotrauma ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUI.Style.Green, 0, 4); } - if (LatchOntoAI.WallAttachPos.HasValue) + if (LatchOntoAI.AttachPos.HasValue) { - //GUI.DrawLine(spriteBatch, pos, - // ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.WallAttachPos.Value.X, -LatchOntoAI.WallAttachPos.Value.Y)), GUI.Style.Green, 0, 3); + GUI.DrawLine(spriteBatch, pos, + ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUI.Style.Green, 0, 3); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 627c70255..37f6c91ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -284,7 +284,7 @@ namespace Barotrauma limb.LastImpactSoundTime = (float)Timing.TotalTime; if (!string.IsNullOrWhiteSpace(limb.HitSoundTag)) { - bool inWater = limb.inWater; + bool inWater = limb.InWater; if (character.CurrentHull != null && character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height + 5.0f && limb.SimPosition.Y < ConvertUnits.ToSimUnits(character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height) + limb.body.GetMaxExtent()) @@ -342,22 +342,41 @@ namespace Barotrauma partial void SetupDrawOrder() { - //make sure every character gets drawn at a distinct "layer" + //make sure every character gets drawn at a distinct "layer" //(instead of having some of the limbs appear behind and some in front of other characters) float startDepth = 0.1f; float increment = 0.001f; foreach (Character otherCharacter in Character.CharacterList) { - if (otherCharacter == character) continue; + if (otherCharacter == character) { continue; } startDepth += increment; } - //make sure each limb has a distinct depth value - List depthSortedLimbs = Limbs.OrderBy(l => l.ActiveSprite == null ? 0.0f : l.ActiveSprite.Depth).ToList(); + //make sure each limb has a distinct depth value + List depthSortedLimbs = Limbs.OrderBy(l => l.DefaultSpriteDepth).ToList(); foreach (Limb limb in Limbs) { - if (limb.ActiveSprite != null) - limb.ActiveSprite.Depth = startDepth + depthSortedLimbs.IndexOf(limb) * 0.00001f; + var sprite = limb.GetActiveSprite(); + if (sprite == null) { continue; } + sprite.Depth = startDepth + depthSortedLimbs.IndexOf(limb) * 0.00001f; + foreach (var conditionalSprite in limb.ConditionalSprites) + { + if (conditionalSprite.Exclusive) + { + conditionalSprite.ActiveSprite.Depth = sprite.Depth; + } + } } + foreach (Limb limb in Limbs) + { + if (limb.ActiveSprite == null) { continue; } + if (limb.Params.InheritLimbDepth == LimbType.None) { continue; } + var matchingLimb = GetLimb(limb.Params.InheritLimbDepth); + if (matchingLimb != null) + { + limb.ActiveSprite.Depth = matchingLimb.ActiveSprite.Depth - 0.0000001f; + } + } + depthSortedLimbs.Reverse(); inversedLimbDrawOrder = depthSortedLimbs.ToArray(); } @@ -562,9 +581,16 @@ namespace Barotrauma if (this is HumanoidAnimController humanoid) { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); + + Vector2 aimPos = humanoid.AimSourceWorldPos; + aimPos.Y = -aimPos.Y; + GUI.DrawLine(spriteBatch, aimPos - Vector2.UnitY * 3, aimPos + Vector2.UnitY * 3, Color.Red); + GUI.DrawLine(spriteBatch, aimPos - Vector2.UnitX * 3, aimPos + Vector2.UnitX * 3, Color.Red); } if (character.MemState.Count > 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index e994f5954..480a5fd26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -1,5 +1,4 @@ -using Barotrauma.Sounds; -using Barotrauma.Particles; +using Barotrauma.Particles; using Microsoft.Xna.Framework; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 0135133fe..3ee39ef88 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -21,7 +21,6 @@ namespace Barotrauma public static bool DebugDrawInteract; protected float soundTimer; - protected float soundInterval; protected float hudInfoTimer = 1.0f; protected bool hudInfoVisible = false; @@ -129,8 +128,52 @@ namespace Barotrauma get { return gibEmitters; } } + private class GUIMessage + { + public string RawText; + public string Identifier; + public string Text; + + private int _value; + public int Value + { + get { return _value; } + set + { + _value = value; + Text = RawText.Replace("[value]", _value.ToString()); + Size = GUI.Font.MeasureString(Text); + } + } + + public Color Color; + public float Lifetime; + public float Timer; + + public Vector2 Size; + + public bool PlaySound; + + public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null) + { + RawText = Text = rawText; + if (value.HasValue) + { + Text = rawText.Replace("[value]", value.Value.ToString()); + Value = value.Value; + } + Timer = -delay; + Size = GUI.Font.MeasureString(Text); + Color = color; + Identifier = identifier; + Lifetime = 3.0f; + } + } + + private List guiMessages = new List(); + public static bool IsMouseOnUI => GUI.MouseOn != null || - (CharacterInventory.IsMouseOnInventory() && !CharacterInventory.DraggingItemToWorld); + (CharacterInventory.IsMouseOnInventory && !CharacterInventory.DraggingItemToWorld); public class ObjectiveEntity { @@ -161,8 +204,7 @@ namespace Barotrauma partial void InitProjSpecific(XElement mainElement) { - soundInterval = mainElement.GetAttributeFloat("soundinterval", 10.0f); - soundTimer = Rand.Range(0.0f, soundInterval); + soundTimer = Rand.Range(0.0f, Params.SoundInterval); sounds = new List(); Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s))); @@ -390,12 +432,7 @@ namespace Barotrauma { if (attackResult.Damage <= 1.0f) { return; } } - - if (soundTimer < soundInterval * 0.5f) - { - PlaySound(CharacterSound.SoundType.Damage); - soundTimer = soundInterval; - } + PlaySound(CharacterSound.SoundType.Damage, maxInterval: 2); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) @@ -412,7 +449,7 @@ namespace Barotrauma if (GameMain.NetworkMember.RespawnManager?.UseRespawnPrompt ?? false) { - CoroutineManager.InvokeAfter(() => + CoroutineManager.Invoke(() => { if (controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } var respawnPrompt = new GUIMessageBox( @@ -470,9 +507,9 @@ namespace Barotrauma } - private List debugInteractablesInRange = new List(); - private List debugInteractablesAtCursor = new List(); - private List> debugInteractablesNearCursor = new List>(); + 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. @@ -568,7 +605,7 @@ namespace Barotrauma if (distanceToItem > closestItemDistance) { continue; } if (!CanInteractWith(item)) { continue; } - debugInteractablesNearCursor.Add(new Pair(item, 1.0f - distanceToItem / (100.0f * aimAssistModifier))); + debugInteractablesNearCursor.Add((item, 1.0f - distanceToItem / (100.0f * aimAssistModifier))); closestItem = item; closestItemDistance = distanceToItem; } @@ -579,31 +616,20 @@ namespace Barotrauma private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f) { Character closestCharacter = null; - float closestDist = 0.0f; maxDist = ConvertUnits.ToSimUnits(maxDist); - + float closestDist = maxDist * maxDist; foreach (Character c in CharacterList) { if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); - if (dist < maxDist * maxDist && (closestCharacter == null || dist < closestDist)) + if (dist < closestDist || + (c.CampaignInteractionType != CampaignMode.InteractionType.None && closestCharacter?.CampaignInteractionType == CampaignMode.InteractionType.None && dist * 0.9f < closestDist)) { closestCharacter = c; closestDist = dist; } - - /*FarseerPhysics.Common.Transform transform; - c.AnimController.Collider.FarseerBody.GetTransform(out transform); - for (int i = 0; i < c.AnimController.Collider.FarseerBody.FixtureList.Count; i++) - { - if (c.AnimController.Collider.FarseerBody.FixtureList[i].Shape.TestPoint(ref transform, ref mouseSimPos)) - { - Console.WriteLine("Hit: " + i); - closestCharacter = c; - } - }*/ } return closestCharacter; @@ -636,9 +662,20 @@ namespace Barotrauma } } + foreach (GUIMessage message in guiMessages) + { + bool wasPending = message.Timer < 0.0f; + message.Timer += deltaTime; + if (wasPending && message.Timer >= 0.0f && message.PlaySound) + { + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + } + } + guiMessages.RemoveAll(m => m.Timer >= m.Lifetime); + if (!enabled) { return; } - if (!IsDead && !IsIncapacitated) + if (!IsIncapacitated) { if (soundTimer > 0) { @@ -649,7 +686,14 @@ namespace Barotrauma switch (enemyAI.State) { case AIState.Attack: - PlaySound(CharacterSound.SoundType.Attack); + if (Rand.Value() > 0.5f) + { + PlaySound(CharacterSound.SoundType.Attack); + } + else + { + PlaySound(CharacterSound.SoundType.Idle); + } break; default: var petBehavior = enemyAI.PetBehavior; @@ -660,7 +704,6 @@ namespace Barotrauma else { PlaySound(CharacterSound.SoundType.Idle); - } break; } @@ -748,6 +791,27 @@ namespace Barotrauma CharacterHUD.Draw(spriteBatch, this, cam); if (drawHealth && !CharacterHUD.IsCampaignInterfaceOpen) { CharacterHealth.DrawHUD(spriteBatch); } } + + public void DrawGUIMessages(SpriteBatch spriteBatch, Camera cam) + { + if (info == null || !Enabled || InvisibleTimer > 0.0f) + { + return; + } + + Vector2 messagePos = DrawPosition; + messagePos.Y += hudInfoHeight; + messagePos = cam.WorldToScreen(messagePos) - Vector2.UnitY * GUI.IntScale(60); + foreach (GUIMessage message in guiMessages) + { + if (message.Timer < 0) { continue; } + Vector2 drawPos = messagePos + Vector2.UnitX * (GUI.IntScale(60) - message.Size.X); + drawPos = new Vector2((int)drawPos.X, (int)drawPos.Y); + float alpha = MathHelper.SmoothStep(1.0f, 0.0f, message.Timer / message.Lifetime); + GUI.DrawString(spriteBatch, drawPos, message.Text, message.Color * alpha); + messagePos -= Vector2.UnitY * message.Size.Y * 1.2f; + } + } public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam) { @@ -827,12 +891,12 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y), new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), Color.White * 0.1f, width: 4); } - foreach (Pair item in debugInteractablesNearCursor) + foreach ((Item item, float dist) in debugInteractablesNearCursor) { GUI.DrawLine(spriteBatch, cursorPos, - new Vector2(item.First.DrawPosition.X, -item.First.DrawPosition.Y), - ToolBox.GradientLerp(item.Second, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); + new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), + ToolBox.GradientLerp(dist, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); } } return; @@ -856,6 +920,7 @@ namespace Barotrauma Vector2 nameSize = GUI.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); @@ -865,18 +930,6 @@ namespace Barotrauma namePos *= viewportSize / screenSize; namePos.X += cam.WorldView.X; namePos.Y -= cam.WorldView.Y; - Color nameColor = Color.White; - if (Controlled != null && TeamID != Controlled.TeamID) - { - if (TeamID == CharacterTeamType.FriendlyNPC) - { - nameColor = UniqueNameColor ?? Color.SkyBlue; - } - else - { - nameColor = GUI.Style.Red; - } - } if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); @@ -931,6 +984,89 @@ namespace Barotrauma } } + public Color GetNameColor() + { + CharacterTeamType team = teamID; + if (Info?.IsDisguisedAsAnother != null) + { + var idCard = Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); + if (idCard != null) + { + if (team == CharacterTeamType.Team2 && idCard.TeamID != CharacterTeamType.Team2) + { + team = CharacterTeamType.Team1; + } + else if (team == CharacterTeamType.Team1 && idCard.TeamID == CharacterTeamType.Team2) + { + team = CharacterTeamType.Team2; + } + } + } + + Color nameColor = GUI.Style.TextColor; + if (Controlled != null && team != Controlled.TeamID) + { + if (TeamID == CharacterTeamType.FriendlyNPC) + { + nameColor = UniqueNameColor ?? Color.SkyBlue; + } + else + { + nameColor = GUI.Style.Red; + } + } + return nameColor; + } + + public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null) + { + GUIMessage existingMessage = null; + + float delay = 0.0f; + if (guiMessages.Any()) + { + delay = guiMessages.Min(m => m.Timer) - 0.5f; + if (delay < 0) + { + delay = -delay; + if (guiMessages.Count > 5) + { + //reduce delays if there's lots of messages + guiMessages.Where(m => m.Timer < 0.0f).ForEach(m => m.Timer *= 0.9f); + } + } + else + { + delay = 0; + } + } + + if (identifier != null) + { + existingMessage = guiMessages.Find(m => m.Identifier == identifier && m.Timer < m.Lifetime * 0.5f); + } + if (existingMessage == null || !value.HasValue) + { + var newMessage = new GUIMessage(rawText, color, delay, identifier, value); + guiMessages.Insert(0, newMessage); + if (playSound) + { + if (delay > 0.0f) + { + newMessage.PlaySound = true; + } + else + { + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + } + } + } + else + { + existingMessage.Value += value.Value; + } + } + /// /// Creates a progress bar that's "linked" to the specified object (or updates an existing one if there's one already linked to the object) /// The progress bar will automatically fade out after 1 sec if the method hasn't been called during that time @@ -958,12 +1094,13 @@ namespace Barotrauma private readonly List matchingSounds = new List(); private SoundChannel soundChannel; - public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f) + public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f, float maxInterval = 0) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } if (GameMain.SoundManager?.Disabled ?? true) { return; } - if (soundTimer > soundInterval * soundIntervalFactor) { return; } + if (soundTimer > Params.SoundInterval * soundIntervalFactor) { return; } + if (Params.SoundInterval - soundTimer < maxInterval) { return; } matchingSounds.Clear(); foreach (var s in sounds) { @@ -975,7 +1112,7 @@ namespace Barotrauma var selectedSound = matchingSounds.GetRandom(); if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling); - soundTimer = soundInterval; + soundTimer = Params.SoundInterval; } public void AddActiveObjectiveEntity(Entity entity, Sprite sprite, Color? color = null) @@ -1028,5 +1165,20 @@ namespace Barotrauma Rand.Range(50.0f, 500.0f), null); } } + + partial void OnMoneyChanged(int prevAmount, int newAmount) + { + if (newAmount > prevAmount) + { + int increase = newAmount - prevAmount; + AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]"), + GUI.Style.Yellow, playSound: this == Controlled, "money", increase); + } + } + + partial void OnTalentGiven(string talentIdentifier) + { + AddMessage(TextManager.Get("talentname." + talentIdentifier.ToString()), GUI.Style.Yellow, playSound: this == Controlled); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index b208ae945..3c8d3d5a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -100,7 +100,7 @@ namespace Barotrauma } } - private static bool shouldRecreateHudTexts = true; + public static bool ShouldRecreateHudTexts { get; set; } = true; private static bool heldDownShiftWhenGotHudTexts; public static bool IsCampaignInterfaceOpen => @@ -150,7 +150,7 @@ namespace Barotrauma } } - if (character.IsHumanoid && character.SelectedCharacter != null) + if (character.Params.CanInteract && character.SelectedCharacter != null) { character.SelectedCharacter.CharacterHealth.AddToGUIUpdateList(); } @@ -195,7 +195,7 @@ namespace Barotrauma } } - if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) + if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { if (character.SelectedCharacter.CanInventoryBeAccessed) { @@ -219,7 +219,7 @@ namespace Barotrauma if (focusedItemOverlayTimer <= 0.0f) { focusedItem = null; - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; } } } @@ -285,6 +285,21 @@ namespace Barotrauma i.GetRootInventoryOwner() == i); } + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + if (!mission.DisplayTargetHudIcons) { continue; } + foreach (var target in mission.HudIconTargets) + { + if (target.Submarine != character.Submarine) { continue; } + float alpha = GetDistanceBasedIconAlpha(target, maxDistance: mission.Prefab.HudIconMaxDistance); + if (alpha <= 0.0f) { continue; } + GUI.DrawIndicator(spriteBatch, target.DrawPosition, cam, 100.0f, mission.Prefab.HudIcon, mission.Prefab.HudIconColor * alpha); + } + } + } + foreach (Character.ObjectiveEntity objectiveEntity in character.ActiveObjectiveEntities) { DrawObjectiveIndicator(spriteBatch, cam, character, objectiveEntity, 1.0f); @@ -317,7 +332,7 @@ namespace Barotrauma if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; } focusedItem = character.FocusedItem; } @@ -342,13 +357,13 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { bool shiftDown = PlayerInput.IsShiftDown(); - if (shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + if (ShouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) { - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; heldDownShiftWhenGotHudTexts = shiftDown; } - var hudTexts = focusedItem.GetHUDTexts(character, shouldRecreateHudTexts); - shouldRecreateHudTexts = false; + var hudTexts = focusedItem.GetHUDTexts(character, ShouldRecreateHudTexts); + ShouldRecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -391,7 +406,26 @@ namespace Barotrauma if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); - GUI.DrawIndicator(spriteBatch, npc.WorldPosition, cam, npc.CurrentHull == character.CurrentHull ? 500.0f : 100.0f, iconStyle.GetDefaultSprite(), iconStyle.Color); + Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); + if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) + { + //TODO: we could probably do better than just hardcoding + //a check for InteractionType.Examine here. + + if (Vector2.DistanceSquared(character.Position, npc.Position) > 500f * 500f) { continue; } + + var body = Submarine.CheckVisibility(character.SimPosition, npc.SimPosition, ignoreLevel: true); + if (body != null && body.UserData as Character != npc) { continue; } + + visibleRange = new Range(-100f, 500f); + } + GUI.DrawIndicator( + spriteBatch, + npc.WorldPosition, + cam, + visibleRange, + iconStyle.GetDefaultSprite(), + iconStyle.Color); } foreach (Item item in Item.ItemList) @@ -400,7 +434,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(character.Position, item.Position) > 500f*500f) { continue; } var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item) { continue; } - GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Vector2(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); + GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Range(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); } } @@ -471,7 +505,7 @@ namespace Barotrauma if (!character.IsIncapacitated && character.Stun <= 0.0f) { - if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) + if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { if (character.SelectedCharacter.CanInventoryBeAccessed) { @@ -525,12 +559,7 @@ namespace Barotrauma textPos -= new Vector2(textSize.X / 2, textSize.Y); - Color nameColor = GUI.Style.TextColor; - if (character.TeamID != character.FocusedCharacter.TeamID) - { - nameColor = character.FocusedCharacter.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; - } - + Color nameColor = character.FocusedCharacter.GetNameColor(); GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); textPos.X += 10.0f * GUI.Scale; textPos.Y += GUI.SubHeadingFont.MeasureString(focusName).Y; @@ -544,11 +573,14 @@ namespace Barotrauma if (character.FocusedCharacter.CanBeDragged) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab)), + string text = character.CanEat ? "EatHint" : "GrabHint"; + GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, GameMain.Config.KeyBindText(InputType.Grab)), GUI.Style.Green, Color.Black, 2, GUI.SmallFont); textPos.Y += largeTextSize.Y; } + if (!character.DisableHealthWindow && + character.IsFriendly(character.FocusedCharacter) && character.FocusedCharacter.CharacterHealth.UseHealthWindow && character.CanInteractWith(character.FocusedCharacter, 160f, false)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index e88192195..24ca41abe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -17,11 +17,19 @@ namespace Barotrauma public bool LastControlled; + #warning TODO: Refactor private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; private Sprite disguisedJobIcon; private Color disguisedJobColor; + private Color disguisedHairColor; + private Color disguisedFacialHairColor; + private Color disguisedSkinColor; + + private Sprite tintMask; + private float tintHighlightThreshold; + private float tintHighlightMultiplier; public static void Init() { @@ -29,6 +37,20 @@ namespace Barotrauma new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } + partial void LoadHeadSpriteProjectSpecific(XElement limbElement) + { + XElement maskElement = limbElement.Element("tintmask"); + if (maskElement != null) + { + string tintMaskPath = maskElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(tintMaskPath)) + { + tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); + tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); + tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f); + } + } + } public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { @@ -143,10 +165,10 @@ 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); + float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y); + DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) @@ -165,21 +187,36 @@ namespace Barotrauma return frame; } - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) + partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } + // if we increased by more than 1 in one increase, then display special color (for talents) + bool specialIncrease = Math.Abs(newLevel - prevLevel) >= 1.0f; + if ((int)newLevel > (int)prevLevel) { int increase = Math.Max((int)newLevel - (int)prevLevel, 1); - GUI.AddMessage( - string.Format("+{0} {1}", increase, TextManager.Get("SkillName." + skillIdentifier)), - GUI.Style.Green, - textPopupPos, - Vector2.UnitY * 10.0f, - playSound: false, - subId: Character?.Submarine?.ID ?? -1); + Character?.AddMessage( + "+[value] "+ TextManager.Get("SkillName." + skillIdentifier), + specialIncrease ? GUI.Style.Orange : GUI.Style.Green, + playSound: Character == Character.Controlled, skillIdentifier, increase); + } + } + + partial void OnExperienceChanged(int prevAmount, int newAmount) + { + if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } + + GameSession.TabMenuInstance?.OnExperienceChanged(Character); + + if (newAmount > prevAmount) + { + int increase = newAmount - prevAmount; + Character?.AddMessage( + "+[value] " + TextManager.Get("experienceshort"), + GUI.Style.Blue, playSound: Character == Character.Controlled, "exp", increase); } } @@ -187,193 +224,36 @@ namespace Barotrauma { if (idCard.Item.Tags == string.Empty) return; - if (idCard.StoredJobPrefab == null || idCard.StoredPortrait == null) + if (idCard.StoredOwnerAppearance.JobPrefab == null || idCard.StoredOwnerAppearance.Portrait == null) { string[] readTags = idCard.Item.Tags.Split(','); - if (readTags.Length == 0) return; + if (readTags.Length == 0) { return; } - if (idCard.StoredJobPrefab == null) + if (idCard.StoredOwnerAppearance.JobPrefab == null) { - string jobIdTag = readTags.FirstOrDefault(s => s.StartsWith("jobid:")); - - if (jobIdTag != null && jobIdTag.Length > 6) - { - string jobId = jobIdTag.Substring(6); - if (jobId != string.Empty) - { - idCard.StoredJobPrefab = JobPrefab.Get(jobId); - } - } + idCard.StoredOwnerAppearance.ExtractJobPrefab(readTags); } - if (idCard.StoredPortrait == null) + if (idCard.StoredOwnerAppearance.Portrait == null) { - string disguisedGender = string.Empty; - string disguisedRace = string.Empty; - string disguisedHeadSpriteId = string.Empty; - int disguisedHairIndex = -1; - int disguisedBeardIndex = -1; - int disguisedMoustacheIndex = -1; - int disguisedFaceAttachmentIndex = -1; - - foreach (string tag in readTags) - { - string[] s = tag.Split(':'); - - switch (s[0]) - { - case "gender": - disguisedGender = s[1]; - break; - - case "race": - disguisedRace = s[1]; - break; - - case "headspriteid": - disguisedHeadSpriteId = s[1]; - break; - - case "hairindex": - disguisedHairIndex = int.Parse(s[1]); - break; - - case "beardindex": - disguisedBeardIndex = int.Parse(s[1]); - break; - - case "moustacheindex": - disguisedMoustacheIndex = int.Parse(s[1]); - break; - - case "faceattachmentindex": - disguisedFaceAttachmentIndex = int.Parse(s[1]); - break; - - case "sheetindex": - string[] vectorValues = s[1].Split(";"); - idCard.StoredSheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); - break; - } - } - - if (disguisedGender == string.Empty || disguisedRace == string.Empty || disguisedHeadSpriteId == string.Empty) - { - idCard.StoredPortrait = null; - idCard.StoredAttachments = null; - return; - } - - foreach (XElement limbElement in Ragdoll.MainElement.Elements()) - { - if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - - XElement spriteElement = limbElement.Element("sprite"); - if (spriteElement == null) { continue; } - - string spritePath = spriteElement.Attribute("texture").Value; - - spritePath = spritePath.Replace("[GENDER]", disguisedGender); - spritePath = spritePath.Replace("[RACE]", disguisedRace.ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId); - - string fileName = Path.GetFileNameWithoutExtension(spritePath); - - //go through the files in the directory to find a matching sprite - foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) - { - if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - string fileWithoutTags = Path.GetFileNameWithoutExtension(file); - fileWithoutTags = fileWithoutTags.Split('[', ']').First(); - if (fileWithoutTags != fileName) { continue; } - idCard.StoredPortrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; - break; - } - - break; - } - - if (Wearables != null) - { - XElement disguisedHairElement, disguisedBeardElement, disguisedMoustacheElement, disguisedFaceAttachmentElement; - List disguisedHairs, disguisedBeards, disguisedMoustaches, disguisedFaceAttachments; - - Gender disguisedGenderEnum = disguisedGender == "female" ? Gender.Female : Gender.Male; - Race disguisedRaceEnum = (Race)Enum.Parse(typeof(Race), disguisedRace); - int headSpriteId = int.Parse(disguisedHeadSpriteId); - - float commonness = disguisedGenderEnum == Gender.Female ? 0.05f : 0.2f; - disguisedHairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Hair, headSpriteId), WearableType.Hair, commonness); - disguisedBeards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Beard, headSpriteId), WearableType.Beard); - disguisedMoustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Moustache, headSpriteId), WearableType.Moustache); - disguisedFaceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.FaceAttachment, headSpriteId), WearableType.FaceAttachment); - - if (IsValidIndex(disguisedHairIndex, disguisedHairs)) - { - disguisedHairElement = disguisedHairs[disguisedHairIndex]; - } - else - { - disguisedHairElement = GetRandomElement(disguisedHairs); - } - if (IsValidIndex(disguisedBeardIndex, disguisedBeards)) - { - disguisedBeardElement = disguisedBeards[disguisedBeardIndex]; - } - else - { - disguisedBeardElement = GetRandomElement(disguisedBeards); - } - - if (IsValidIndex(disguisedMoustacheIndex, disguisedMoustaches)) - { - disguisedMoustacheElement = disguisedMoustaches[disguisedMoustacheIndex]; - } - else - { - disguisedMoustacheElement = GetRandomElement(disguisedMoustaches); - } - if (IsValidIndex(disguisedFaceAttachmentIndex, disguisedFaceAttachments)) - { - disguisedFaceAttachmentElement = disguisedFaceAttachments[disguisedFaceAttachmentIndex]; - } - else - { - disguisedFaceAttachmentElement = GetRandomElement(disguisedFaceAttachments); - } - - idCard.StoredAttachments = new List(); - - disguisedFaceAttachmentElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.FaceAttachment))); - disguisedBeardElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Beard))); - disguisedMoustacheElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Moustache))); - disguisedHairElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Hair))); - - if (OmitJobInPortraitClothing) - { - JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); - } - else - { - idCard.StoredJobPrefab?.ClothingElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); - } - } + idCard.StoredOwnerAppearance.ExtractAppearance(this, readTags); } } - if (idCard.StoredJobPrefab != null) + if (idCard.StoredOwnerAppearance.JobPrefab != null) { - disguisedJobIcon = idCard.StoredJobPrefab.Icon; - disguisedJobColor = idCard.StoredJobPrefab.UIColor; + disguisedJobIcon = idCard.StoredOwnerAppearance.JobPrefab.Icon; + disguisedJobColor = idCard.StoredOwnerAppearance.JobPrefab.UIColor; } - disguisedPortrait = idCard.StoredPortrait; - disguisedSheetIndex = idCard.StoredSheetIndex; - disguisedAttachmentSprites = idCard.StoredAttachments; + disguisedPortrait = idCard.StoredOwnerAppearance.Portrait; + disguisedSheetIndex = idCard.StoredOwnerAppearance.SheetIndex; + disguisedAttachmentSprites = idCard.StoredOwnerAppearance.Attachments; + + disguisedHairColor = idCard.StoredOwnerAppearance.HairColor; + disguisedFacialHairColor = idCard.StoredOwnerAppearance.FacialHairColor; + disguisedSkinColor = idCard.StoredOwnerAppearance.SkinColor; } partial void LoadAttachmentSprites(bool omitJob) @@ -422,43 +302,107 @@ namespace Barotrauma public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false) { - if (evaluateDisguise && IsDisguised) return; + if (evaluateDisguise && IsDisguised) { return; } Vector2? sheetIndex; Sprite portraitToDraw; List attachmentsToDraw; + Color hairColor; + Color facialHairColor; + Color skinColor; + if (!IsDisguisedAsAnother || !evaluateDisguise) { sheetIndex = Head.SheetIndex; portraitToDraw = Portrait; attachmentsToDraw = AttachmentSprites; + + hairColor = Head.HairColor; + facialHairColor = Head.FacialHairColor; + skinColor = Head.SkinColor; } else { sheetIndex = disguisedSheetIndex; portraitToDraw = disguisedPortrait; attachmentsToDraw = disguisedAttachmentSprites; + + hairColor = disguisedHairColor; + facialHairColor = disguisedFacialHairColor; + skinColor = disguisedSkinColor; } if (portraitToDraw != null) { + var currEffect = spriteBatch.GetCurrentEffect(); // Scale down the head sprite 10% float scale = targetWidth * 0.9f / Portrait.size.X; if (sheetIndex.HasValue) { + SetHeadEffect(spriteBatch); portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size); } - portraitToDraw.Draw(spriteBatch, screenPos + offset, Color.White, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + portraitToDraw.Draw(spriteBatch, screenPos + offset, skinColor, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); if (attachmentsToDraw != null) { float depthStep = 0.000001f; foreach (var attachment in attachmentsToDraw) { - DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + SetAttachmentEffect(spriteBatch, attachment); + DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, GetAttachmentColor(attachment, hairColor, facialHairColor), flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); depthStep += depthStep; } } + spriteBatch.SwapEffect(currEffect); + } + } + + //TODO: I hate this so much :( + private SpriteBatch.EffectWithParams headEffectParameters; + private Dictionary attachmentEffectParameters + = new Dictionary(); + + private void SetHeadEffect(SpriteBatch spriteBatch) + { + headEffectParameters.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + headEffectParameters.Params ??= new Dictionary(); + headEffectParameters.Params["xBaseTexture"] = HeadSprite.Texture; + headEffectParameters.Params["xTintMaskTexture"] = tintMask?.Texture ?? GUI.WhiteTexture; + headEffectParameters.Params["xCutoffTexture"] = GUI.WhiteTexture; + headEffectParameters.Params["baseToCutoffSizeRatio"] = 1.0f; + headEffectParameters.Params["highlightThreshold"] = tintHighlightThreshold; + headEffectParameters.Params["highlightMultiplier"] = tintHighlightMultiplier; + spriteBatch.SwapEffect(headEffectParameters); + } + + private void SetAttachmentEffect(SpriteBatch spriteBatch, WearableSprite attachment) + { + if (!attachmentEffectParameters.ContainsKey(attachment.Type)) + { + attachmentEffectParameters.Add(attachment.Type, new SpriteBatch.EffectWithParams(GameMain.GameScreen.ThresholdTintEffect, new Dictionary())); + } + var parameters = attachmentEffectParameters[attachment.Type].Params; + parameters["xBaseTexture"] = attachment.Sprite.Texture; + parameters["xTintMaskTexture"] = GUI.WhiteTexture; + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + parameters["highlightThreshold"] = tintHighlightThreshold; + parameters["highlightMultiplier"] = tintHighlightMultiplier; + spriteBatch.SwapEffect(attachmentEffectParameters[attachment.Type]); + } + + private Color GetAttachmentColor(WearableSprite attachment, Color hairColor, Color facialHairColor) + { + switch (attachment.Type) + { + case WearableType.Hair: + return hairColor; + case WearableType.Beard: + case WearableType.Moustache: + return facialHairColor; + default: + return Color.White; } } @@ -467,34 +411,28 @@ namespace Barotrauma var headSprite = HeadSprite; if (headSprite != null) { + var currEffect = spriteBatch.GetCurrentEffect(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); if (Head.SheetIndex.HasValue) { headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); } - headSprite.Draw(spriteBatch, screenPos, scale: scale); + SetHeadEffect(spriteBatch); + headSprite.Draw(spriteBatch, screenPos, scale: scale, color: SkinColor); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { - DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep); + SetAttachmentEffect(spriteBatch, attachment); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, HairColor, FacialHairColor)); depthStep += depthStep; } } + spriteBatch.SwapEffect(currEffect); } } - public void DrawJobIcon(SpriteBatch spriteBatch, Vector2 pos, float scale = 1.0f, bool evaluateDisguise = false) - { - if (evaluateDisguise && IsDisguised) return; - var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; - if (icon == null) { return; } - Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; - - icon.Draw(spriteBatch, pos, iconColor, scale: scale); - } - public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; @@ -505,7 +443,7 @@ namespace Barotrauma icon.Draw(spriteBatch, area.Center.ToVector2(), iconColor, scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); } - private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) + private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, Color? color = null, SpriteEffects spriteEffects = SpriteEffects.None) { if (attachment.InheritSourceRect) { @@ -522,7 +460,7 @@ namespace Barotrauma attachment.Sprite.SourceRect = head.SourceRect; } } - Vector2 origin = attachment.Sprite.Origin; + Vector2 origin; if (attachment.InheritOrigin) { origin = head.Origin; @@ -537,7 +475,7 @@ namespace Barotrauma { depth = head.Depth - depthStep; } - attachment.Sprite.Draw(spriteBatch, drawPos, Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); + attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) @@ -552,6 +490,9 @@ namespace Barotrauma int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); int faceAttachmentIndex = inc.ReadByte(); + Color skinColor = inc.ReadColorR8G8B8(); + Color hairColor = inc.ReadColorR8G8B8(); + Color facialHairColor = inc.ReadColorR8G8B8(); string ragdollFile = inc.ReadString(); string jobIdentifier = inc.ReadString(); @@ -577,6 +518,9 @@ namespace Barotrauma ID = infoID, }; ch.RecreateHead(headSpriteID,(Race)race, (Gender)gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ch.SkinColor = skinColor; + ch.HairColor = hairColor; + ch.FacialHairColor = facialHairColor; if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) @@ -591,7 +535,449 @@ namespace Barotrauma } ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); } + + byte savedStatValueCount = inc.ReadByte(); + for (int i = 0; i < savedStatValueCount; i++) + { + int statType = inc.ReadByte(); + string statIdentifier = inc.ReadString(); + float statValue = inc.ReadSingle(); + bool removeOnDeath = inc.ReadBoolean(); + ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); + } + ch.ExperiencePoints = inc.ReadUInt16(); + ch.AdditionalTalentPoints = inc.ReadUInt16(); return ch; } + + public void CreateIcon(RectTransform rectT) + { + LoadHeadAttachments(); + new GUICustomComponent(rectT, + onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); + } + + public class AppearanceCustomizationMenu : IDisposable + { + public readonly CharacterInfo CharacterInfo; + public GUIListBox HeadSelectionList = null; + public bool HasIcon = true; + + public GUIScrollBar.OnMovedHandler OnSliderMoved = null; + public GUIScrollBar.OnMovedHandler OnSliderReleased = null; + public Action OnHeadSwitch = null; + + private readonly GUIComponent parentComponent; + private readonly List characterSprites = new List(); + public GUIButton RandomizeButton; + + public AppearanceCustomizationMenu(CharacterInfo info, GUIComponent parent, bool hasIcon = true) + { + CharacterInfo = info; + parentComponent = parent; + HasIcon = hasIcon; + + RecreateFrameContents(); + } + + public void RecreateFrameContents() + { + var info = CharacterInfo; + + HeadSelectionList = null; + parentComponent.ClearChildren(); + ClearSprites(); + + float contentWidth = HasIcon ? 0.75f : 1.0f; + var listBox = new GUIListBox( + new RectTransform(new Vector2(contentWidth, 1.0f), parentComponent.RectTransform, + Anchor.CenterLeft)) + { CanBeFocused = false, CanTakeKeyBoardFocus = false }; + var content = listBox.Content; + + info.LoadHeadAttachments(); + if (HasIcon) + { + info.CreateIcon( + new RectTransform(new Vector2(0.25f, 1.0f), parentComponent.RectTransform, Anchor.CenterRight) + { RelativeOffset = new Vector2(-0.01f, 0.0f) }); + } + + RectTransform createItemRectTransform(string labelTag, float width = 0.6f) + { + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.166f), content.RectTransform)); + + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), + TextManager.Get(labelTag), font: GUI.SubHeadingFont); + + var bottomItem = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), + style: null); + + return new RectTransform(new Vector2(width, 1.0f), bottomItem.RectTransform, Anchor.Center); + } + + RectTransform genderItemRT = createItemRectTransform("Gender", 1.0f); + + GUILayoutGroup genderContainer = + new GUILayoutGroup(genderItemRT, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + void createGenderButton(Gender gender) + { + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), + TextManager.Get(gender.ToString()), style: "ListBoxElement") + { + UserData = gender, + OnClicked = OpenHeadSelection, + Selected = info.Gender == gender + }; + } + + createGenderButton(Gender.Male); + createGenderButton(Gender.Female); + + int countAttachmentsOfType(WearableType wearableType) + => info.FilterByTypeAndHeadID( + info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), + wearableType, info.HeadSpriteId).Count(); + + List attachmentSliders = new List(); + void createAttachmentSlider(int initialValue, WearableType wearableType) + { + int attachmentCount = countAttachmentsOfType(wearableType); + if (attachmentCount > 0) + { + var labelTag = wearableType == WearableType.FaceAttachment + ? "FaceAttachment.Accessories" + : $"FaceAttachment.{wearableType}"; + var sliderItemRT = createItemRectTransform(labelTag); + var slider = + new GUIScrollBar(sliderItemRT, style: "GUISlider") + { + Range = new Vector2(0, attachmentCount), + StepValue = 1, + OnMoved = (bar, scroll) => SwitchAttachment(bar, wearableType), + OnReleased = OnSliderReleased, + BarSize = 1.0f / (float)(attachmentCount + 1) + }; + slider.BarScrollValue = initialValue; + attachmentSliders.Add(slider); + } + } + + createAttachmentSlider(info.HairIndex, WearableType.Hair); + createAttachmentSlider(info.BeardIndex, WearableType.Beard); + createAttachmentSlider(info.MoustacheIndex, WearableType.Moustache); + createAttachmentSlider(info.FaceAttachmentIndex, WearableType.FaceAttachment); + + void createColorSelector(string labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, + Action setter) + { + var selectorItemRT = createItemRectTransform(labelTag, 0.4f); + var dropdown = + new GUIDropDown(selectorItemRT) + { AllowNonText = true }; + + var listBoxSize = dropdown.ListBox.RectTransform.RelativeSize; + dropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.75f, listBoxSize.Y); + var dropdownButton = dropdown.GetChild(); + var buttonFrame = + new GUIFrame( + new RectTransform(Vector2.One * 0.7f, dropdownButton.RectTransform, Anchor.CenterLeft) + { RelativeOffset = new Vector2(0.05f, 0.0f) }, style: null); + Color? previewingColor = null; + dropdown.OnSelected = (component, color) => + { + previewingColor = null; + setter((Color)color); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + return true; + }; + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + + dropdown.ListBox.UseGridLayout = true; + foreach (var option in options) + { + var optionElement = + new GUIFrame( + new RectTransform(new Vector2(0.25f, 1.0f / 3.0f), + dropdown.ListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = option.Color, + CanBeFocused = true + }; + var colorElement = + new GUIFrame( + new RectTransform(Vector2.One * 0.75f, optionElement.RectTransform, Anchor.Center, + scaleBasis: ScaleBasis.Smallest), + style: null) + { + Color = option.Color, + HoverColor = option.Color, + OutlineColor = Color.Lerp(Color.Black, option.Color, 0.5f), + CanBeFocused = false + }; + } + + var childToSelect = dropdown.ListBox.Content.FindChild(c => (Color)c.UserData == getter()); + dropdown.Select(dropdown.ListBox.Content.GetChildIndex(childToSelect)); + + //The following exists to track mouseover to preview colors before selecting them + new GUICustomComponent(new RectTransform(Vector2.One, buttonFrame.RectTransform), + onUpdate: (deltaTime, component) => + { + if (GUI.MouseOn is GUIFrame { Parent: { } p } hoveredFrame && dropdown.ListBox.Content.IsParentOf(hoveredFrame)) + { + previewingColor ??= getter(); + Color color = (Color)(dropdown.ListBox.Content.FindChild(c => + c == hoveredFrame || c.IsParentOf(hoveredFrame))?.UserData ?? dropdown.SelectedData ?? getter()); + setter(color); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + } + else if (previewingColor.HasValue) + { + setter(previewingColor.Value); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + previewingColor = null; + } + }, onDraw: null) + { + CanBeFocused = false, + Visible = true + }; + } + + if (countAttachmentsOfType(WearableType.Hair) > 0) + { + createColorSelector($"Customization.{nameof(info.HairColor)}", info.HairColors, + () => info.HairColor, (color) => info.HairColor = color); + } + + if (countAttachmentsOfType(WearableType.Moustache) > 0 || + countAttachmentsOfType(WearableType.Beard) > 0) + { + createColorSelector($"Customization.{nameof(info.FacialHairColor)}", info.FacialHairColors, + () => info.FacialHairColor, (color) => info.FacialHairColor = color); + } + + createColorSelector($"Customization.{nameof(info.SkinColor)}", info.SkinColors, () => info.SkinColor, + (color) => info.SkinColor = color); + + RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, + parentComponent.RectTransform, + anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) + { RelativeOffset = new Vector2(0.01f, 0.005f) }, style: "RandomizeButton") + { + OnClicked = (button, o) => + { + info.Head = new HeadInfo(); + info.SetGenderAndRace(Rand.RandSync.Unsynced); + info.SetColors(); + + RecreateFrameContents(); + info.RefreshHead(); + OnHeadSwitch?.Invoke(this); + attachmentSliders.ForEach(s => OnSliderMoved?.Invoke(s, s.BarScroll)); + + return false; + } + }; + //force update twice because the listbox is insanely janky + //TODO: fix all of the UI :) + listBox.ForceUpdate(); + listBox.ForceUpdate(); + foreach (var childLayoutGroup in listBox.Content.GetAllChildren()) + { + childLayoutGroup.Recalculate(); + } + } + + private bool OpenHeadSelection(GUIButton button, object userData) + { + Gender selectedGender = (Gender)userData; + + var info = CharacterInfo; + + float characterHeightWidthRatio = info.HeadSprite.size.Y / info.HeadSprite.size.X; + HeadSelectionList ??= new GUIListBox( + new RectTransform( + new Point(parentComponent.Rect.Width, + (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f)), GUI.Canvas) + { + AbsoluteOffset = new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, + button.Rect.Bottom) + }); + HeadSelectionList.Visible = true; + HeadSelectionList.Content.ClearChildren(); + ClearSprites(); + + parentComponent.RectTransform.SizeChanged += () => + { + if (parentComponent == null || HeadSelectionList?.RectTransform == null || button == null) + { + return; + } + + HeadSelectionList.RectTransform.Resize(new Point(parentComponent.Rect.Width, + (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f))); + HeadSelectionList.RectTransform.AbsoluteOffset = + new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, button.Rect.Bottom); + }; + + new GUIFrame( + new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), + style: "OuterGlow", color: Color.Black) + { + UserData = "outerglow", + CanBeFocused = false + }; + + GUILayoutGroup row = null; + int itemsInRow = 0; + + XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); + XElement headSpriteElement = headElement.Element("sprite"); + string spritePathWithTags = headSpriteElement.Attribute("texture").Value; + + var characterConfigElement = info.CharacterConfigElement; + + var heads = info.Heads; + if (heads != null) + { + row = null; + itemsInRow = 0; + foreach (var kvp in heads.Where(kv => kv.Key.Gender == selectedGender)) + { + var headPreset = kvp.Key; + Race race = headPreset.Race; + int headIndex = headPreset.ID; + + string spritePath = spritePathWithTags + .Replace("[GENDER]", selectedGender.ToString().ToLowerInvariant()) + .Replace("[RACE]", race.ToString().ToLowerInvariant()); + + if (!File.Exists(spritePath)) + { + continue; + } + + Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); + headSprite.SourceRect = + new Rectangle(CalculateOffset(headSprite, kvp.Value.ToPoint()), + headSprite.SourceRect.Size); + characterSprites.Add(headSprite); + + if (itemsInRow >= 4 || row == null) + { + row = new GUILayoutGroup( + new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), + true) + { + UserData = selectedGender, + Visible = true + }; + itemsInRow = 0; + } + + var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), + style: "ListBoxElementSquare") + { + OutlineColor = Color.White * 0.5f, + PressedColor = Color.White * 0.5f, + UserData = new Tuple(selectedGender, race, headIndex), + OnClicked = SwitchHead, + Selected = selectedGender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Visible = true + }; + + new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); + itemsInRow++; + } + } + + return false; + } + + private bool SwitchHead(GUIButton button, object obj) + { + var info = CharacterInfo; + Gender gender = ((Tuple)obj).Item1; + Race race = ((Tuple)obj).Item2; + int id = ((Tuple)obj).Item3; + info.Gender = gender; + info.Race = race; + info.Head.HeadSpriteId = id; + RecreateFrameContents(); + OnHeadSwitch?.Invoke(this); + return true; + } + + private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) + { + var info = CharacterInfo; + int index = (int)scrollBar.BarScrollValue; + switch (type) + { + case WearableType.Beard: + info.BeardIndex = index; + break; + case WearableType.FaceAttachment: + info.FaceAttachmentIndex = index; + break; + case WearableType.Hair: + info.HairIndex = index; + break; + case WearableType.Moustache: + info.MoustacheIndex = index; + break; + default: + DebugConsole.ThrowError($"Wearable type not implemented: {type}"); + return false; + } + + info.RefreshHead(); + OnSliderMoved?.Invoke(scrollBar, scrollBar.BarScroll); + return true; + } + + public void Update() + { + if (HeadSelectionList != null && PlayerInput.PrimaryMouseButtonDown() && + !GUI.IsMouseOn(HeadSelectionList)) + { + HeadSelectionList.Visible = false; + } + } + + public void AddToGUIUpdateList() + { + HeadSelectionList?.AddToGUIUpdateList(); + } + + private void ClearSprites() + { + foreach (Sprite sprite in characterSprites) { sprite.Remove(); } + characterSprites.Clear(); + } + + public void Dispose() + { + ClearSprites(); + } + + ~AppearanceCustomizationMenu() + { + Dispose(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 0798a00d3..5897105f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -119,15 +119,23 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 3); + msg.WriteRangedInteger(0, 0, 4); Inventory.ClientWrite(msg, extraData); break; case NetEntityEvent.Type.Treatment: - msg.WriteRangedInteger(1, 0, 3); + msg.WriteRangedInteger(1, 0, 4); msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 3); + msg.WriteRangedInteger(2, 0, 4); + break; + case NetEntityEvent.Type.UpdateTalents: + msg.WriteRangedInteger(3, 0, 4); + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.Prefab.UIntIdentifier); + } break; } } @@ -258,7 +266,7 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); - (AIController as EnemyAIController)?.PetBehavior?.ClientRead(msg); + AIController?.ClientRead(msg); } msg.ReadPadBits(); @@ -291,7 +299,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 9); + int eventType = msg.ReadRangedInteger(0, 13); switch (eventType) { case 0: //NetEntityEvent.Type.InventoryState @@ -350,7 +358,7 @@ namespace Barotrauma { string skillIdentifier = msg.ReadString(); float skillLevel = msg.ReadSingle(); - info?.SetSkillLevel(skillIdentifier, skillLevel, Position + Vector2.UnitY * 150.0f); + info?.SetSkillLevel(skillIdentifier, skillLevel); } break; case 4: // NetEntityEvent.Type.SetAttackTarget @@ -382,11 +390,12 @@ namespace Barotrauma } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; } - if (attackLimb?.attack != null) + if (attackLimb?.attack != null && Controlled != this) { if (eventType == 4) { SetAttackTarget(attackLimb, targetEntity, targetSimPos); + PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); } else { @@ -450,6 +459,36 @@ namespace Barotrauma } } break; + case 10: //NetEntityEvent.Type.UpdateExperience + int experienceAmount = msg.ReadInt32(); + info?.SetExperience(experienceAmount); + break; + case 11: //NetEntityEvent.Type.UpdateTalents: + ushort talentCount = msg.ReadUInt16(); + for (int i = 0; i < talentCount; i++) + { + bool addedThisRound = msg.ReadBoolean(); + UInt32 talentIdentifier = msg.ReadUInt32(); + GiveTalent(talentIdentifier, addedThisRound); + } + break; + case 12: //NetEntityEvent.Type.UpdateMoney: + int moneyAmount = msg.ReadInt32(); + SetMoney(moneyAmount); + break; + case 13: //NetEntityEvent.Type.UpdatePermanentStats: + byte savedStatValueCount = msg.ReadByte(); + StatTypes statType = (StatTypes)msg.ReadByte(); + info?.ClearSavedStatValues(statType); + for (int i = 0; i < savedStatValueCount; i++) + { + string statIdentifier = msg.ReadString(); + float statValue = msg.ReadSingle(); + bool removeOnDeath = msg.ReadBoolean(); + info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); + } + break; + } msg.ReadPadBits(); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index 5835cd675..f4a5f282c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -1,10 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { partial class AfflictionHusk : Affliction { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 4dc901c4f..9ee0fa4ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -30,7 +30,7 @@ namespace Barotrauma get { return alignment; } set { - if (alignment == value) return; + if (alignment == value) { return; } alignment = value; UpdateAlignment(); } @@ -48,116 +48,24 @@ namespace Barotrauma private float bloodParticleTimer; - // healing interface - private GUIFrame healthInterfaceFrame; - private GUIFrame healthWindow; private GUITextBlock deadIndicator; - private GUIComponent lowSkillIndicator; + //private GUIComponent lowSkillIndicator; - private GUILayoutGroup cprLayout; - private GUIFrame cprFrame; private GUIButton cprButton; private GUIListBox afflictionTooltip; private static readonly Color oxygenLowGrainColor = new Color(0.1f, 0.1f, 0.1f, 1f); - private struct HeartratePosition - { - public float Time; - public float Height; - - public HeartratePosition ScaleHeight(float scale) - { - return new HeartratePosition - { - Time = this.Time, - Height = this.Height * scale - }; - } - - public HeartratePosition ScaleTime(float scale) - { - return new HeartratePosition - { - Time = this.Time * scale, - Height = this.Height - }; - } - - public HeartratePosition AddTime(float time) - { - return new HeartratePosition - { - Time = this.Time + time, - Height = this.Height - }; - } - - public static IEnumerable ScaleAndDisplace(IEnumerable positions, float heightScale, float timeScale, float timeAdd) - { - HeartratePosition prevPos = new HeartratePosition - { - Time = 0.0f, - Height = 0.0f - }; - bool wrapped = false; - foreach (HeartratePosition pos in positions) - { - HeartratePosition newPos = pos.ScaleHeight(heightScale).ScaleTime(timeScale).AddTime(timeAdd); - if (newPos.Time > 1.0f) - { - if (!wrapped) - { - yield return new HeartratePosition - { - Time = 1.0f, - Height = (newPos.Height - prevPos.Height) / (newPos.Time - prevPos.Time) * (1.0f - prevPos.Time) + prevPos.Height - }; - yield return new HeartratePosition - { - Time = 0.0f, - Height = (newPos.Height - prevPos.Height) / (newPos.Time - prevPos.Time) * (1.0f - prevPos.Time) + prevPos.Height - }; - wrapped = true; - } - newPos.Time -= 1.0f; - } - prevPos = newPos; - yield return newPos; - } - } - } - private List heartratePositions; - private float currentHeartrateTime; - private float heartbeatTimer; - private static Texture2D heartrateFade; - - private readonly HeartratePosition[] heartbeatPattern = - { - new HeartratePosition() { Time = 0.0f, Height = 0.0f }, - new HeartratePosition() { Time = 0.15f, Height = 0.2f }, - new HeartratePosition() { Time = 0.2f, Height = -0.2f }, - new HeartratePosition() { Time = 0.36f, Height = 0.0f }, - new HeartratePosition() { Time = 0.43f, Height = 0.8f }, - new HeartratePosition() { Time = 0.57f, Height = -0.8f }, - new HeartratePosition() { Time = 0.64f, Height = 0.0f }, - new HeartratePosition() { Time = 0.8f, Height = 0.2f }, - new HeartratePosition() { Time = 0.85f, Height = -0.2f }, - new HeartratePosition() { Time = 1.0f, Height = 0.0f }, - }; - private SpriteSheet limbIndicatorOverlay; private float limbIndicatorOverlayAnimState; private SpriteSheet medUIExtra; private float medUIExtraAnimState; - private GUIComponent draggingMed; - private int highlightedLimbIndex = -1; private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; @@ -166,12 +74,9 @@ namespace Barotrauma private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; - private GUIFrame afflictionInfoFrame; private GUIListBox afflictionIconContainer; - private GUIListBox afflictionInfoContainer; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; - private GUITextBlock selectedLimbText; private float distortTimer; @@ -211,7 +116,6 @@ namespace Barotrauma if (prevOpenHealthWindow != null) { - prevOpenHealthWindow.selectedLimbIndex = -1; prevOpenHealthWindow.highlightedLimbIndex = -1; } @@ -255,6 +159,12 @@ namespace Barotrauma get { return cprButton; } } + public GUIComponent InventorySlotContainer + { + get; + private set; + } + public float HealthBarPulsateTimer { get { return healthBarPulsateTimer; } @@ -279,56 +189,118 @@ namespace Barotrauma character.OnAttacked += OnAttacked; - bool horizontal = true; + healthWindow = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.6f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox"); - healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) + var healthWindowVerticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), healthWindow.RectTransform, Anchor.Center)) { - HoverCursor = CursorState.Hand + Stretch = true }; - healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; - healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; - healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; - - healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: Color.Green, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical", showFrame: false) + var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), healthWindowVerticalLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) { - IsHorizontal = horizontal - }; - healthBarShadow.Visible = false; - healthShadowSize = 1.0f; - - healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical") - { - HoverCursor = CursorState.Hand, - ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), - Enabled = true, - IsHorizontal = horizontal + Stretch = true }; - healthInterfaceFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.55f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "ItemUI"); + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), + onDraw: (spriteBatch, component) => + { + character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, character != Character.Controlled); + }); + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) + { + AutoScaleHorizontal = true + }; + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), + onDraw: (spriteBatch, component) => + { + character.Info?.DrawJobIcon(spriteBatch, component.Rect, character != Character.Controlled); + }); - var healthInterfaceLayout = new GUILayoutGroup(new RectTransform(Vector2.One / 1.05f, healthInterfaceFrame.RectTransform, anchor: Anchor.Center), true); - var healthWindowContainer = new GUIFrame(new RectTransform(new Vector2(0.45f, 1.0f), healthInterfaceLayout.RectTransform), style: null); + var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); + var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); + healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), + barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + { + IsHorizontal = true + }; + healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), + barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + { + IsHorizontal = true + }; - //limb selection frame - healthWindow = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), healthWindowContainer.RectTransform, Anchor.CenterRight, Pivot.CenterRight), style: "GUIFrameListBox"); + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), healthWindowVerticalLayout.RectTransform), style: null); - var healthWindowVerticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, healthWindow.RectTransform, Anchor.Center)) + var characterIndicatorArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), healthWindowVerticalLayout.RectTransform), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.03f + //RelativeSpacing = 0.05f }; - var paddedHealthWindow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), healthWindowVerticalLayout.RectTransform), isHorizontal: true) + InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight), + (spriteBatch, component) => + { + for (int i = 0; i < character.Inventory.Capacity; i++) + { + if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface || Character.Controlled != Character) { continue; } + + //don't draw the item if it's being dragged out of the slot + bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); + + Inventory.DrawSlot(spriteBatch, Character.Inventory, Character.Inventory.visualSlots[i], Character.Inventory.GetItemAt(i), i, drawItem, Character.Inventory.SlotTypes[i]); + + if (medUIExtra != null) + { + float overlayScale = Math.Min( + Character.Inventory.visualSlots[i].Rect.Width / (float)medUIExtra.FrameSize.X, + Character.Inventory.visualSlots[i].Rect.Height / (float)medUIExtra.FrameSize.Y); + + int frame = (int)medUIExtraAnimState; + + medUIExtra.Draw(spriteBatch, frame, Character.Inventory.visualSlots[i].Rect.Center.ToVector2(), Color.Gray, origin: medUIExtra.FrameSize.ToVector2() / 2, rotate: 0.0f, + scale: Vector2.One * overlayScale); + } + } + }, + (dt, component) => + { + if (!GameMain.Instance.Paused) + { + medUIExtraAnimState = (medUIExtraAnimState + dt * 10.0f) % 16.0f; + } + }); + + + cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") { - Stretch = true, - RelativeSpacing = 0.03f + OnClicked = (button, userData) => + { + Character selectedCharacter = Character.Controlled?.SelectedCharacter; + if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) + { + return false; + } + + Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? + AnimController.Animation.None : AnimController.Animation.CPR; + + selectedCharacter.AnimController.ResetPullJoints(); + + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + } + + return true; + }, + ToolTip = TextManager.Get("doctor.cprobjective"), + IgnoreLayoutGroups = true, + Visible = false }; - var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), paddedHealthWindow.RectTransform), + var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), (spriteBatch, component) => { DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true); @@ -353,172 +325,52 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - var rightSide = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), paddedHealthWindow.RectTransform), style: null); + afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); - new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.3f), rightSide.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - (sb, component) => - { - if (medUIExtra == null) { return; } - float overlayScale = Math.Min( - component.Rect.Width / (float)medUIExtra.FrameSize.X, - component.Rect.Height / (float)medUIExtra.FrameSize.Y); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), + TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); - int frame = (int)medUIExtraAnimState; - - medUIExtra.Draw(sb, frame, component.Rect.Center.ToVector2(), Color.Gray, origin: medUIExtra.FrameSize.ToVector2() / 2, rotate: 0.0f, - scale: Vector2.One * overlayScale); - }, - (dt, component) => - { - if (!GameMain.Instance.Paused) - { - medUIExtraAnimState = (medUIExtraAnimState + dt * 10.0f) % 16.0f; - } - }); - - GUILayoutGroup selectedLimbLayout = new GUILayoutGroup(new RectTransform(Vector2.One, rightSide.RectTransform)); - - selectedLimbText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.08f), selectedLimbLayout.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Center) - { - AutoScaleHorizontal = true - }; - - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.92f), selectedLimbLayout.RectTransform), style: null) - { - KeepSpaceForScrollBar = true - }; - - var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); - - var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); - - healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") - { - IsHorizontal = true - }; - healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") - { - IsHorizontal = true - }; - - //affliction info frame - afflictionInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 1.0f), healthInterfaceLayout.RectTransform), style: null); - var paddedInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), afflictionInfoFrame.RectTransform, Anchor.Center), style: null); - - var infoLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedInfoFrame.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.03f - }; - - var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), infoLayout.RectTransform), style: "GUIFrameListBox"); - - var textLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), textContainer.RectTransform, Anchor.Center, Pivot.Center)) - { - Stretch = true, - RelativeSpacing = 0.03f, - CanBeFocused = true - }; - - textLayout.RectTransform.RelativeOffset = new Vector2(0, 0.025f); - - var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), textLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) - { - Stretch = true - }; - - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), - onDraw: (spriteBatch, component) => - { - character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, openHealthWindow?.Character != Character.Controlled); - }); - characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - AutoScaleHorizontal = true - }; - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), - onDraw: (spriteBatch, component) => - { - character.Info?.DrawJobIcon(spriteBatch, component.Rect, openHealthWindow?.Character != Character.Controlled); - }); - - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: "HorizontalLine"); - - afflictionInfoContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), textLayout.RectTransform, Anchor.TopLeft), style: null); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: "HorizontalLine"); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textLayout.RectTransform, Anchor.TopLeft), TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont); - - treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), textLayout.RectTransform), true) + treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), healthWindowVerticalLayout.RectTransform), true) { Stretch = false }; - recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(0.9f, 1.0f), treatmentLayout.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: true, style: null) + recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), treatmentLayout.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: true, style: null) { - KeepSpaceForScrollBar = false + Spacing = GUI.IntScale(4), + KeepSpaceForScrollBar = false, + ScrollBarVisible = false, + AutoHideScrollBar = false + }; + new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) + { + CanBeFocused = false }; - lowSkillIndicator = new GUIImage(new RectTransform(new Vector2(0.1f, 1.0f), treatmentLayout.RectTransform, Anchor.TopLeft, Pivot.Center), - style: "GUINotificationButton") + characterIndicatorArea.Recalculate(); + + healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) + { + HoverCursor = CursorState.Hand + }; + + healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; + healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; + healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; + + healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: Color.Green, style: "CharacterHealthBar", showFrame: false) { - ToolTip = TextManager.Get("lowmedicalskillwarning"), - Color = GUI.Style.Orange, - HoverColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.5f), - PressedColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.5f), Visible = false }; - lowSkillIndicator.RectTransform.MaxSize = new Point(lowSkillIndicator.Rect.Height); + healthShadowSize = 1.0f; - var tempFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: null); - - cprLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), infoLayout.RectTransform), true) + healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: "CharacterHealthBar") { - Stretch = true - }; - - cprFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 1.0f), cprLayout.RectTransform), style: "GUIFrameListBox"); - - heartrateFade ??= TextureLoader.FromFile("Content/UI/Health/HeartrateFade.png"); - - new GUICustomComponent(new RectTransform(Vector2.One * 0.95f, cprFrame.RectTransform, Anchor.Center), DrawHeartrate, UpdateHeartrate); - - heartbeatTimer = 0.46f; - - heartratePositions = new List - { - heartbeatPattern.First(), - heartbeatPattern.Last() - }; - - cprButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), cprLayout.RectTransform, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") - { - OnClicked = (button, userData) => - { - Character selectedCharacter = Character.Controlled?.SelectedCharacter; - if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) - { - return false; - } - - Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? - AnimController.Animation.None : AnimController.Animation.CPR; - - selectedCharacter.AnimController.ResetPullJoints(); - - if (GameMain.Client != null) - { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); - } - - return true; - }, - ToolTip = TextManager.Get("doctor.cprobjective"), - Visible = false + HoverCursor = CursorState.Hand, + ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), + Enabled = true }; UpdateAlignment(); @@ -542,8 +394,8 @@ namespace Barotrauma } else { - var causeOfDeath = GetCauseOfDeath(); - Character.Controlled.Kill(causeOfDeath.First, causeOfDeath.Second); + var (type, affliction) = GetCauseOfDeath(); + Character.Controlled.Kill(type, affliction); Character.Controlled = null; } } @@ -568,6 +420,8 @@ namespace Barotrauma } } } + + healthWindowVerticalLayout.Recalculate(); } private void OnAttacked(Character attacker, AttackResult attackResult) @@ -598,15 +452,15 @@ namespace Barotrauma switch (alignment) { case Alignment.Left: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomLeft); + healthWindow.RectTransform.SetPosition(Anchor.BottomLeft); break; case Alignment.Right: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomRight); + healthWindow.RectTransform.SetPosition(Anchor.BottomRight); break; } - healthInterfaceFrame.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); - healthInterfaceFrame.RectTransform.RecalculateChildren(false); + healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); + healthWindow.RectTransform.RecalculateChildren(false); } public void UpdateClientSpecific(float deltaTime) @@ -683,19 +537,33 @@ namespace Barotrauma float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); + + Vector2 velocity = Rand.Vector(affliction.Strength * 0.1f); if (!inWater) { bloodParticleSize *= 2.0f; + velocity = targetLimb.LinearVelocity * 100.0f; } // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, - targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); + targetLimb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); - if (blood != null) + if (blood != null && !inWater) { blood.Size *= bloodParticleSize; + if (!string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) + { + blood.OnCollision += (Vector2 pos, Hull hull) => + { + var decal = hull?.AddDecal(Character.BloodDecalName, pos, Rand.Range(1.0f, 2.0f), isNetworkEvent: true); + if (decal != null) + { + decal.FadeTimer = decal.LifeTime - decal.FadeOutTime * 2; + } + }; + } } bloodParticleTimer = MathHelper.Lerp(2, 0.5f, severity); } @@ -731,8 +599,13 @@ namespace Barotrauma .FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { - int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); - return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(a1.Strength - a1.Strength); + int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); + if (dmgPerSecond != 0) { return dmgPerSecond; } + return Math.Sign(GetStr(a1) - GetStr(a2)); + static float GetStr(Affliction affliction) + { + return affliction.Strength / affliction.Prefab.MaxStrength * (affliction.Prefab.IsBuff ? 1.0f : 10.0f); + } }); HintManager.OnAfflictionDisplayed(Character, currentDisplayedAfflictions); updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; @@ -785,7 +658,8 @@ namespace Barotrauma if (afflictionGrainStrength > 0.0f) { grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); - grainColor = Color.Lerp(grainColor, Color.White, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); + Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; + grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } } foreach (LimbHealth limbHealth in limbHealths) @@ -832,7 +706,7 @@ namespace Barotrauma } else if (openHealthWindow == this) { - if (HUD.CloseHUD(healthInterfaceFrame.Rect)) + if (HUD.CloseHUD(healthWindow.Rect)) { //emulate a Health input to get the character to deselect the item server-side if (GameMain.Client != null) @@ -842,9 +716,22 @@ namespace Barotrauma OpenHealthWindow = null; } - if (GUI.MouseOn != null && GUI.MouseOn.UserData is string str && str == "selectaffliction") + foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) { - Affliction affliction = GUI.MouseOn.Parent.UserData as Affliction; + if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } + if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) + { + afflictionIcon.Flash(GUI.Style.Red); + } + else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) + { + afflictionIcon.Flash(GUI.Style.Green); + } + } + + if (GUI.MouseOn?.UserData is Affliction) + { + Affliction affliction = GUI.MouseOn?.UserData as Affliction; if (afflictionTooltip == null || afflictionTooltip.UserData != affliction) { @@ -858,7 +745,17 @@ namespace Barotrauma int height = afflictionTooltip.Content.Children.Sum(c => c.Rect.Height) + 10; afflictionTooltip.RectTransform.Resize(new Point(afflictionTooltip.Rect.Width, height), true); - afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.Right, GUI.MouseOn.Rect.Y); + if (Alignment == Alignment.Right) + { + afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.X, GUI.MouseOn.Rect.Y); + afflictionTooltip.RectTransform.Pivot = Pivot.TopRight; + } + else + { + afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.Right, GUI.MouseOn.Rect.Y); + afflictionTooltip.RectTransform.Anchor = Anchor.TopLeft; + } + afflictionTooltip.ScrollBarVisible = false; var labelContainer = afflictionTooltip.Content.GetChildByUserData("label"); @@ -901,6 +798,19 @@ namespace Barotrauma UpdateAfflictionContainer(selectedLimb); currentDisplayedLimb = selectedLimb; } + + UpdateAfflictionInfos(displayedAfflictions.Select(d => d.affliction)); + + foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) + { + var treatmentButton = component.GetChild(); + if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } + treatmentButton.Enabled = Character.Controlled.Inventory.AllItems.Any(it => it.prefab == itemPrefab); + foreach (GUIComponent child in treatmentButton.Children) + { + child.Enabled = treatmentButton.Enabled; + } + } } if (Character.IsDead) @@ -939,16 +849,6 @@ namespace Barotrauma openHealthWindow = null; } - lowSkillIndicator.Visible = Character.Controlled != null && Character.Controlled.GetSkillLevel("medical") < 50.0f; - lowSkillIndicator.IgnoreLayoutGroups = !lowSkillIndicator.Visible; - - recommendedTreatmentContainer.RectTransform.Resize(new Vector2(0.9f, 1.0f)); - lowSkillIndicator.RectTransform.Resize(new Vector2(0.1f, 1.0f)); - - treatmentLayout.Recalculate(); - - lowSkillIndicator.Color = new Color(lowSkillIndicator.Color, MathHelper.Lerp(0.5f, 1.0f, (float)(Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 2.0f)); - if (Inventory.DraggingItems.Any()) { if (highlightedLimbIndex > -1) @@ -956,20 +856,6 @@ namespace Barotrauma selectedLimbIndex = highlightedLimbIndex; } } - - if (draggingMed != null) - { - if (!PlayerInput.PrimaryMouseButtonHeld()) - { - OnItemDropped(draggingMed.UserData as Item, ignoreMousePos: false); - draggingMed = null; - } - } - - /*if (GUI.MouseOn?.UserData is Affliction affliction) - { - ShowAfflictionInfo(affliction, afflictionInfoContainer); - }*/ } else { @@ -1024,17 +910,11 @@ namespace Barotrauma && !Character.IsDead && Character.IsKnockedDown && openHealthWindow == this; - cprButton.IgnoreLayoutGroups = !cprButton.Visible; cprButton.Selected = Character.Controlled != null && Character == Character.Controlled.SelectedCharacter && Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; - cprFrame.RectTransform.Resize(new Vector2(0.7f, 1.0f)); - cprButton.RectTransform.Resize(new Vector2(1.0f, 1.0f)); - - cprLayout.Recalculate(); - deadIndicator.Visible = Character.IsDead; } @@ -1043,7 +923,7 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } if (OpenHealthWindow == this) { - healthInterfaceFrame.AddToGUIUpdateList(); + healthWindow.AddToGUIUpdateList(); afflictionTooltip?.AddToGUIUpdateList(); } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) @@ -1071,6 +951,16 @@ namespace Barotrauma UpdateAlignment(); } + foreach (Affliction affliction in afflictions) + { + if (affliction.Prefab.AfflictionOverlay != null) + { + Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; + ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * (affliction.GetAfflictionOverlayMultiplier()), Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); + } + } + float damageOverlayAlpha = DamageOverlayTimer; if (Vitality < MaxVitality * 0.1f) { @@ -1100,22 +990,26 @@ namespace Barotrauma } - private Pair highlightedAfflictionIcon = null; + private (Affliction affliction, string text)? highlightedAfflictionIcon; public void DrawStatusHUD(SpriteBatch spriteBatch) { highlightedAfflictionIcon = null; //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - List> statusIcons = new List>(); - if (Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 5.0f) - statusIcons.Add(new Pair(pressureAffliction, TextManager.Get("PressureHUDWarning"))); + List<(Affliction affliction, string text)> statusIcons = new List<(Affliction affliction, string text)>(); + if (Character.InPressure) + { + statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); + } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) - statusIcons.Add(new Pair(oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + { + statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + } foreach (Affliction affliction in currentDisplayedAfflictions) { - statusIcons.Add(new Pair(affliction, affliction.Prefab.Name)); + statusIcons.Add((affliction, affliction.Prefab.Name)); } Vector2 highlightedIconPos = Vector2.Zero; @@ -1132,9 +1026,9 @@ namespace Barotrauma Point pos = new Point(afflictionArea.Right - iconSize, afflictionArea.Top); - foreach (Pair statusIcon in statusIcons) + foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.First; + Affliction affliction = statusIcon.affliction; AfflictionPrefab afflictionPrefab = affliction.Prefab; Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); @@ -1154,12 +1048,6 @@ namespace Barotrauma GUI.Style.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); } - /*var slot = GUI.Style.GetComponentStyle("AfflictionIconSlot"); - slot.Sprites[highlightedIcon == statusIcon ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None][0].Draw( - spriteBatch, afflictionIconRect, - highlightedIcon == statusIcon ? slot.HoverColor : slot.Color);*/ - - float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; afflictionPrefab.Icon?.Draw(spriteBatch, @@ -1177,7 +1065,7 @@ namespace Barotrauma if (highlightedAfflictionIcon != null) { - string nameTooltip = highlightedAfflictionIcon.Second; + string nameTooltip = highlightedAfflictionIcon.Value.text; Vector2 offset = GUI.Font.MeasureString(nameTooltip); GUI.DrawString(spriteBatch, @@ -1242,72 +1130,75 @@ namespace Barotrauma public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); + private readonly List<(Affliction affliction, float strength)> displayedAfflictions = new List<(Affliction affliction, float strength)>(); + private void UpdateAfflictionContainer(LimbHealth selectedLimb) { - selectedLimbText.Text = selectedLimb == null ? "" : selectedLimb.Name; - if (selectedLimb == null) { afflictionIconContainer.Content.ClearChildren(); return; } var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.ShouldShowIcon(Character)); - var displayedAfflictions = afflictionIconContainer.Content.Children.Select(c => c.UserData as Affliction); - if (currentAfflictions.Any(a => !displayedAfflictions.Contains(a)) || - displayedAfflictions.Any(a => !currentAfflictions.Contains(a))) + if (currentAfflictions.Any(a => !displayedAfflictions.Any(d => d.affliction == a)) || + displayedAfflictions.Any(a => !currentAfflictions.Contains(a.affliction))) { CreateAfflictionInfos(currentAfflictions); + CreateRecommendedTreatments(); + } + //update recommended treatments if the strength of some displayed affliction has changed by > 1 + else if (displayedAfflictions.Any(d => Math.Abs(d.strength - currentAfflictions.First(a => a == d.affliction).Strength) > 1.0f)) + { + CreateRecommendedTreatments(); } - - UpdateAfflictionInfos(displayedAfflictions); } private void CreateAfflictionInfos(IEnumerable afflictions) { afflictionIconContainer.ClearChildren(); - afflictionInfoContainer.ClearChildren(); - afflictionInfoContainer.UserData = null; - recommendedTreatmentContainer.Content.ClearChildren(); - - float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); + displayedAfflictions.Clear(); - //random variance is 200% when the skill is 0 - //no random variance if the skill is 50 or more - float randomVariance = MathHelper.Lerp(2.0f, 0.0f, characterSkillLevel / 50.0f); - - //key = item identifier - //float = suitability - Dictionary treatmentSuitability = new Dictionary(); - GetSuitableTreatments(treatmentSuitability, normalize: true, randomization: randomVariance); - - //Affliction mostSevereAffliction = afflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !afflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? afflictions.FirstOrDefault(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) { - var child = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform, Anchor.TopCenter)) - { - Stretch = true, - UserData = affliction - }; + displayedAfflictions.Add((affliction, affliction.Strength)); - var button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.9f), child.RectTransform), style: null) + var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform), style: "ListBoxElement") { - Color = Color.Gray.Multiply(0.1f).Opaque(), - HoverColor = Color.Gray.Multiply(0.4f).Opaque(), - SelectedColor = Color.Gray.Multiply(0.25f).Opaque(), - PressedColor = Color.Gray.Multiply(0.2f).Opaque(), - UserData = "selectaffliction", + UserData = affliction, OnClicked = SelectAffliction }; + new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false }; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + CanBeFocused = false + }; + + var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUI.Style.Green, style: "GUIAfflictionBar") + { + UserData = "afflictionstrengthprediction", + CanBeFocused = false + }; + new GUIProgressBar(new RectTransform(Vector2.One, progressbarBg.RectTransform), 0.0f, Color.Transparent, showFrame: false, style: "GUIAfflictionBar") + { + UserData = "afflictionstrength", + CanBeFocused = false + }; + + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), style: null) { CanBeFocused = false }; + if (affliction == mostSevereAffliction) { - buttonToSelect = button; + buttonToSelect = frame; } - var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, button.RectTransform, Anchor.Center), affliction.Prefab.Icon, scaleToFit: true) + var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, content.RectTransform), affliction.Prefab.Icon, scaleToFit: true) { Color = GetAfflictionIconColor(affliction), CanBeFocused = false @@ -1316,33 +1207,68 @@ namespace Barotrauma afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f); afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f); - float afflictionVitalityDecrease = affliction.GetVitalityDecrease(this); - - Color afflictionEffectColor = Color.White; - if (afflictionVitalityDecrease > 0.0f) + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content.RectTransform), + affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.BottomCenter) { - afflictionEffectColor = GUI.Style.Red; - } - else if (afflictionVitalityDecrease < 0.0f) - { - afflictionEffectColor = GUI.Style.Green; - } - - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), child.RectTransform), - affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.Center, style: "GUIToolTip"); + CanBeFocused = false + }; nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); - - new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.1f), child.RectTransform), 0.0f, afflictionEffectColor, style: "GUIAfflictionBar") + nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y)); + nameText.RectTransform.SizeChanged += () => { - UserData = "afflictionstrength" + nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); }; - child.Recalculate(); + content.Recalculate(); } - buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); - + buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); afflictionIconContainer.RecalculateChildren(); + } + + private void CreateRecommendedTreatments() + { + ItemPrefab prevHighlightedItem = null; + if (GUI.MouseOn?.UserData is ItemPrefab && recommendedTreatmentContainer.Content.IsParentOf(GUI.MouseOn)) + { + prevHighlightedItem = (ItemPrefab)GUI.MouseOn.UserData; + } + + recommendedTreatmentContainer.Content.ClearChildren(); + + float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); + + //key = item identifier + //float = suitability + Dictionary treatmentSuitability = new Dictionary(); + GetSuitableTreatments(treatmentSuitability, + normalize: true, + ignoreHiddenAfflictions: true, + limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); + + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + //prefer suggestions for items the player has + if (Character.Controlled.Inventory.FindItemByIdentifier(treatment) != null) + { + treatmentSuitability[treatment] *= 10.0f; + } + } + + if (!treatmentSuitability.Any()) + { + new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + recommendedTreatmentContainer.ScrollBarVisible = false; + recommendedTreatmentContainer.AutoHideScrollBar = false; + } + else + { + recommendedTreatmentContainer.ScrollBarVisible = true; + recommendedTreatmentContainer.AutoHideScrollBar = true; + } List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); @@ -1353,27 +1279,55 @@ namespace Barotrauma if (count > 5) { break; } if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; } - var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 7.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), + var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), style: null) { UserData = item }; - var innerFrame = new GUIFrame(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox") + var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader") { - CanBeFocused = false + UserData = item, + ToolTip = $"‖color:255,255,255,255‖{item.Name}‖color:end‖" + '\n' + item.Description, + DisabledColor = Color.White * 0.1f, + OnClicked = (btn, userdata) => + { + if (!(userdata is ItemPrefab itemPrefab)) { return false; } + var item = Character.Controlled.Inventory.AllItems.FirstOrDefault(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); + return true; + } }; + + new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow") + { + CanBeFocused = false, + Color = GUI.Style.Green, + HoverColor = Color.White, + PressedColor = Color.DarkGray, + SelectedColor = Color.Transparent, + DisabledColor = Color.Transparent + }; + Sprite itemSprite = item.InventoryIcon ?? item.sprite; Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), innerFrame.RectTransform, Anchor.Center), itemSprite, scaleToFit: true) { CanBeFocused = false, - Color = itemColor, + Color = itemColor * 0.9f, HoverColor = itemColor, - SelectedColor = itemColor + SelectedColor = itemColor, + DisabledColor = itemColor * 0.8f }; - itemSlot.ToolTip = item.Name; + + if (item == prevHighlightedItem) + { + innerFrame.State = GUIComponent.ComponentState.Hover; + innerFrame.Children.ForEach(c => c.State = GUIComponent.ComponentState.Hover); + } } recommendedTreatmentContainer.RecalculateChildren(); @@ -1386,10 +1340,18 @@ namespace Barotrauma return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength); }); - //afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) => - //{ - // return Math.Sign(((Affliction)r2.GUIComponent.UserData).GetVitalityDecrease(this) - ((Affliction)r1.GUIComponent.UserData).GetVitalityDecrease(this)); - //}); + if (count > 0) + { + var treatmentIconSize = recommendedTreatmentContainer.Content.Children.Sum(c => c.Rect.Width + recommendedTreatmentContainer.Spacing); + if (treatmentIconSize < recommendedTreatmentContainer.Content.Rect.Width) + { + var spacing = new GUIFrame(new RectTransform(new Point((recommendedTreatmentContainer.Content.Rect.Width - treatmentIconSize) / 2, 0), recommendedTreatmentContainer.Content.RectTransform), style: null) + { + CanBeFocused = false + }; + spacing.RectTransform.SetAsFirstChild(); + } + } } private void CreateAfflictionInfoElements(GUIComponent parent, Affliction affliction) @@ -1446,7 +1408,6 @@ namespace Barotrauma affliction.Strength / affliction.Prefab.MaxStrength); description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); - //labelContainer.Recalculate(); int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); if (vitalityDecrease == 0) @@ -1466,21 +1427,7 @@ namespace Barotrauma private bool SelectAffliction(GUIButton button, object userData) { - Affliction affliction = button.Parent.UserData as Affliction; - bool selected = button.Selected; - - afflictionInfoContainer.UserData = null; - afflictionInfoContainer.ClearChildren(); - if (!selected) - { - afflictionInfoContainer.UserData = affliction; - - CreateAfflictionInfoElements(afflictionInfoContainer.Content, affliction); - - afflictionInfoContainer.RecalculateChildren(); - } - foreach (var child in afflictionIconContainer.Content.Children) { GUIButton btn = child.GetChild(); @@ -1493,137 +1440,52 @@ namespace Barotrauma return false; } - private void UpdateHeartrate(float deltaTime, GUICustomComponent component) - { - if (GameMain.Instance.Paused) { return; } - - heartbeatTimer -= deltaTime * 0.5f; - - if (heartbeatTimer <= 0.0f) - { - while (heartbeatTimer <= 0.0f) { heartbeatTimer += 0.5f; } - - IEnumerable newPositions; - if (Character == null || Character.IsDead || Character.IsUnconscious) - { - newPositions = Enumerable.Repeat(new HeartratePosition { Time = currentHeartrateTime, Height = 0.0f }, 1); - } - else - { - newPositions = HeartratePosition.ScaleAndDisplace(heartbeatPattern, 1.0f, 0.1f, currentHeartrateTime); - } - - float visibleRangeStart = currentHeartrateTime - 0.35f; - if (visibleRangeStart < 0.0f) - { - visibleRangeStart += 1.0f; - } - heartratePositions.RemoveAll(hp => (hp.Time < visibleRangeStart || hp.Time > currentHeartrateTime) && - ((hp.Time < visibleRangeStart && hp.Time > currentHeartrateTime) || visibleRangeStart < currentHeartrateTime)); - - heartratePositions.AddRange(newPositions); - - if (!heartratePositions.Any(hp => hp.Time >= 1.0f)) - { - heartratePositions.Add(new HeartratePosition { Time = 1.0f, Height = 0.0f }); - } - if (!heartratePositions.Any(hp => hp.Time <= 0.0f)) - { - heartratePositions.Add(new HeartratePosition { Time = 0.0f, Height = 0.0f }); - } - } - - currentHeartrateTime += deltaTime * 0.5f; - while (currentHeartrateTime >= 1.0f) - { - currentHeartrateTime -= 1.0f; - } - } - - private void DrawHeartrate(SpriteBatch spriteBatch, GUICustomComponent component) - { - Rectangle targetRect = component.Parent.Rect; - targetRect.Location += new Point(6, 6); - targetRect.Size -= new Point(12, 12); - - //GUI.DrawRectangle(spriteBatch, targetRect, Color.Black, true); - - bool first = true; - Vector2 prevPos = Vector2.Zero; - foreach (var heartratePosition in heartratePositions.OrderBy(hp => hp.Time)) - { - Vector2 pos = new Vector2(heartratePosition.Time, -heartratePosition.Height * 0.5f + 0.5f) * targetRect.Size.ToVector2() + targetRect.Location.ToVector2(); - - if (pos.X < targetRect.Left + 1) { pos.X = targetRect.Left + 1; } - if (pos.X > targetRect.Right - 1) { pos.X = targetRect.Right - 1; } - - if (first) - { - first = false; - } - else - { - int thickness = (int)(GUI.Scale * 2.5f); - if (thickness < 1) { thickness = 1; } - GUI.DrawLine(spriteBatch, prevPos, pos, Color.Lime, 0, thickness); - GUI.DrawLine(spriteBatch, prevPos + new Vector2(0.0f, 1.0f), pos + new Vector2(0.0f, 1.0f), Color.Lime * 0.5f, 0, thickness); - GUI.DrawLine(spriteBatch, prevPos - new Vector2(0.0f, 1.0f), pos - new Vector2(0.0f, 1.0f), Color.Lime * 0.5f, 0, thickness); - } - - prevPos = pos; - } - - Rectangle sourceRect = heartrateFade.Bounds; - - Rectangle destinationRectangle = new Rectangle( - new Point((int)(currentHeartrateTime * targetRect.Width) + targetRect.Left - targetRect.Height, targetRect.Top), - new Point((int)(targetRect.Height * ((float)sourceRect.Width / (float)sourceRect.Height)), targetRect.Height)); - - if (destinationRectangle.Left < targetRect.Left) - { - Rectangle destinationRectangle2 = new Rectangle(); - destinationRectangle2.Location = new Point(targetRect.Right - (targetRect.Left - destinationRectangle.Left), targetRect.Top); - destinationRectangle2.Size = new Point(targetRect.Right - destinationRectangle2.Left, targetRect.Height); - - int originalWidth = sourceRect.Width; - sourceRect.Width = (int)(sourceRect.Width * ((float)(destinationRectangle.Right - targetRect.Left) / (float)targetRect.Height)); - sourceRect.X += originalWidth - sourceRect.Width; - - Rectangle sourceRect2 = heartrateFade.Bounds; - sourceRect2.Width -= sourceRect.Width; - spriteBatch.Draw(heartrateFade, destinationRectangle2, sourceRect2, Color.White); - - originalWidth = destinationRectangle.Width; - int newWidth = destinationRectangle.Right - targetRect.Left; - - destinationRectangle.Size = new Point(newWidth, targetRect.Height); - destinationRectangle.X += originalWidth - newWidth; - - GUI.DrawRectangle(spriteBatch, new Rectangle(destinationRectangle.Right, destinationRectangle.Top, - destinationRectangle2.Left - destinationRectangle.Right, destinationRectangle2.Height), Color.Black, true); - } - else - { - GUI.DrawRectangle(spriteBatch, new Rectangle(destinationRectangle.Right, destinationRectangle.Top, - targetRect.Right - destinationRectangle.Right, destinationRectangle.Height), Color.Black, true); - GUI.DrawRectangle(spriteBatch, new Rectangle(targetRect.Left, destinationRectangle.Top, - destinationRectangle.Left - targetRect.Left, destinationRectangle.Height), Color.Black, true); - } - - spriteBatch.Draw(heartrateFade, destinationRectangle, sourceRect, Color.White); - } - private void UpdateAfflictionInfos(IEnumerable afflictions) { + var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); + if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) + { + potentialTreatment = Character.Controlled.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + } + potentialTreatment ??= Inventory.SelectedSlot?.Item; + foreach (Affliction affliction in afflictions) { - var child = afflictionIconContainer.Content.FindChild(affliction); - var afflictionStrengthBar = child.GetChildByUserData("afflictionstrength") as GUIProgressBar; - afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength; - - if (afflictionInfoContainer.UserData == affliction) + float afflictionVitalityDecrease = affliction.GetVitalityDecrease(this); + Color afflictionEffectColor = Color.White; + if (afflictionVitalityDecrease > 0.0f) { - UpdateAfflictionInfo(afflictionInfoContainer.Content, affliction); + afflictionEffectColor = GUI.Style.Red; + } + else if (afflictionVitalityDecrease < 0.0f) + { + afflictionEffectColor = GUI.Style.Green; + } + + var child = afflictionIconContainer.Content.FindChild(affliction); + + var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; + afflictionStrengthPredictionBar.BarSize = 0.0f; + var afflictionStrengthBar = afflictionStrengthPredictionBar.GetChildByUserData("afflictionstrength") as GUIProgressBar; + afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength; + afflictionStrengthBar.Color = afflictionEffectColor; + + float afflictionStrengthPrediction = GetAfflictionStrengthPrediction(potentialTreatment, affliction); + if (!MathUtils.NearlyEqual(afflictionStrengthPrediction, affliction.Strength)) + { + float t = (float)Math.Max(0.5f, (Math.Sin(Timing.TotalTime * 5) + 1.0f) / 2.0f); + if (afflictionStrengthPrediction < affliction.Strength) + { + afflictionStrengthBar.Color = afflictionEffectColor; + afflictionStrengthPredictionBar.Color = GUI.Style.Blue * t; + afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize; + afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; + } + else + { + afflictionStrengthPredictionBar.Color = Color.Red * t; + afflictionStrengthPredictionBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; + } } if (afflictionTooltip != null && afflictionTooltip.UserData == affliction) @@ -1633,6 +1495,32 @@ namespace Barotrauma } } + private float GetAfflictionStrengthPrediction(Item item, Affliction affliction) + { + float strength = affliction.Strength; + if (item == null) { return strength; } + + foreach (ItemComponent ic in item.Components) + { + if (ic.statusEffectLists == null) { continue; } + if (!ic.statusEffectLists.TryGetValue(ActionType.OnUse, out List statusEffects)) { continue; } + foreach (StatusEffect effect in statusEffects) + { + foreach (var reduceAffliction in effect.ReduceAffliction) + { + if (reduceAffliction.affliction != affliction.Identifier && reduceAffliction.affliction != affliction.Prefab.AfflictionType) { continue; } + strength -= reduceAffliction.amount * (effect.Duration > 0 ? effect.Duration : 1.0f); + } + foreach (var addAffliction in effect.Afflictions) + { + if (addAffliction.Prefab != affliction.Prefab) { continue; } + strength += addAffliction.Strength * (effect.Duration > 0 ? effect.Duration : 1.0f); + } + } + } + return strength; + } + private void UpdateAfflictionInfo(GUIComponent parent, Affliction affliction) { var labelContainer = parent.GetChildByUserData("label"); @@ -1664,8 +1552,7 @@ namespace Barotrauma { //items can be dropped outside the health window if (!ignoreMousePos && - !healthWindow.Rect.Contains(PlayerInput.MousePosition) && - !afflictionInfoFrame.Rect.Contains(PlayerInput.MousePosition)) + !healthWindow.Rect.Contains(PlayerInput.MousePosition) ) { return false; } @@ -1687,35 +1574,6 @@ namespace Barotrauma return true; } - - private List GetAvailableMedicalItems() - { - List allInventoryItems = new List(); - allInventoryItems.AddRange(Character.Inventory.AllItems); - if (Character.SelectedCharacter?.Inventory != null && Character.CanAccessInventory(Character.SelectedCharacter.Inventory)) - { - allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.AllItems); - } - if (Character.SelectedBy?.Inventory != null) - { - allInventoryItems.AddRange(Character.SelectedBy.Inventory.AllItems); - } - List medicalItems = new List(); - foreach (Item item in allInventoryItems) - { - foreach (Item containedItem in item.ContainedItems) - { - if (!containedItem.HasTag("medical") && !containedItem.HasTag("chem")) { continue; } - medicalItems.Add(containedItem); - } - - if (!item.HasTag("medical") && !item.HasTag("chem")) { continue; } - medicalItems.Add(item); - } - - return medicalItems.Distinct().ToList(); - } - private void UpdateLimbIndicators(float deltaTime, Rectangle drawArea) { if (!GameMain.Instance.Paused) @@ -1743,10 +1601,6 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked() && highlightedLimbIndex > -1) { selectedLimbIndex = highlightedLimbIndex; - //afflictionContainer.ClearChildren(); - afflictionIconContainer.ClearChildren(); - afflictionInfoContainer.ClearChildren(); - afflictionInfoContainer.UserData = null; } } @@ -1917,30 +1771,23 @@ namespace Barotrauma i++; } - if (selectedLimbIndex > -1 && selectedLimbText != null) + if (selectedLimbIndex > -1 && afflictionIconContainer.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, - new Vector2(selectedLimbText.Rect.X, selectedLimbText.Rect.Center.Y), + new Vector2(afflictionIconContainer.Rect.X, afflictionIconContainer.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } } - - if (draggingMed != null) - { - GUIImage itemImage = draggingMed.GetChild(); - float scale = Math.Min(40.0f / itemImage.Sprite.size.X, 40.0f / itemImage.Sprite.size.Y); - itemImage.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, itemImage.Color, 0, scale); - } } private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) { - if (!affliction.ShouldShowIcon(Character)) { return; } + if (!affliction.ShouldShowIcon(Character) || affliction.Prefab.Icon == null) { return; } Vector2 iconSize = affliction.Prefab.Icon.size * iconScale; float showIconThreshold = Character.Controlled?.CharacterHealth == this ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold; @@ -1968,9 +1815,9 @@ namespace Barotrauma healthBarHolder.Visible = value; } - private readonly List> newAfflictions = new List>(); - private readonly List> newLimbAfflictions = new List>(); - private readonly List> newPeriodicEffects = new List>(); + private readonly List<(AfflictionPrefab afflictionPrefab, float strength)> newAfflictions = new List<(AfflictionPrefab afflictionPrefab, float strength)>(); + private readonly List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)> newLimbAfflictions = new List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)>(); + private readonly List<(AfflictionPrefab.PeriodicEffect effect, float timer)> newPeriodicEffects = new List<(AfflictionPrefab.PeriodicEffect effect, float timer)>(); public void ClientRead(IReadMessage inc) { @@ -1997,41 +1844,41 @@ namespace Barotrauma for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); - newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newAfflictions.Add(new Pair(afflictionPrefab, afflictionStrength)); + newAfflictions.Add((afflictionPrefab, afflictionStrength)); } foreach (Affliction affliction in afflictions) { //deactivate afflictions that weren't included in the network message - if (!newAfflictions.Any(a => a.First == affliction.Prefab)) + if (!newAfflictions.Any(a => a.afflictionPrefab == affliction.Prefab)) { affliction.Strength = 0.0f; } } - foreach (Pair newAffliction in newAfflictions) + foreach (var (afflictionPrefab, strength) in newAfflictions) { - Affliction existingAffliction = afflictions.Find(a => a.Prefab == newAffliction.First); + Affliction existingAffliction = afflictions.Find(a => a.Prefab == afflictionPrefab); if (existingAffliction == null) { - existingAffliction = newAffliction.First.Instantiate(newAffliction.Second); + existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction); } - existingAffliction.SetStrength(newAffliction.Second); + existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) { Character.SetStun(existingAffliction.Strength, true, true); } foreach (var periodicEffect in newPeriodicEffects) { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } //timer has wrapped around, apply the effect - if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; - foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; + foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: null); } @@ -2063,9 +1910,9 @@ namespace Barotrauma for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); - newPeriodicEffects.Add(new Pair(afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); + newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } - newLimbAfflictions.Add(new Triplet(limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); + newLimbAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } foreach (LimbHealth limbHealth in limbHealths) @@ -2073,33 +1920,33 @@ namespace Barotrauma foreach (Affliction affliction in limbHealth.Afflictions) { //deactivate afflictions that weren't included in the network message - if (!newLimbAfflictions.Any(a => a.First == limbHealth && a.Second == affliction.Prefab)) + if (!newLimbAfflictions.Any(a => a.limb == limbHealth && a.afflictionPrefab == affliction.Prefab)) { affliction.Strength = 0.0f; } } - foreach (Triplet newAffliction in newLimbAfflictions) + foreach (var (limb, afflictionPrefab, strength) in newLimbAfflictions) { - if (newAffliction.First != limbHealth) { continue; } - Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == newAffliction.Second); + if (limb != limbHealth) { continue; } + Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == afflictionPrefab); if (existingAffliction == null) { - existingAffliction = newAffliction.Second.Instantiate(newAffliction.Third); + existingAffliction = afflictionPrefab.Instantiate(strength); limbHealth.Afflictions.Add(existingAffliction); } - existingAffliction.SetStrength(newAffliction.Third); + existingAffliction.SetStrength(strength); foreach (var periodicEffect in newPeriodicEffects) { - if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.First)) { continue; } + if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } //timer has wrapped around, apply the effect - if (periodicEffect.Second - existingAffliction.PeriodicEffectTimers[periodicEffect.First] > periodicEffect.First.MinInterval / 2) + if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { - existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; - foreach (StatusEffect effect in periodicEffect.First.StatusEffects) + existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; + foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { - Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(newAffliction.First)); + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } @@ -2122,8 +1969,8 @@ namespace Barotrauma if (limbHealths[limb.HealthIndex].Afflictions.Count == 0) continue; foreach (Affliction a in limbHealths[limb.HealthIndex].Afflictions) { - limb.BurnOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.BurnOverlayAlpha; - limb.DamageOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.DamageOverlayAlpha; + limb.BurnOverlayStrength += a.Strength / Math.Min(a.Prefab.MaxStrength, 100) * a.Prefab.BurnOverlayAlpha; + limb.DamageOverlayStrength += a.Strength / Math.Min(a.Prefab.MaxStrength, 100) * a.Prefab.DamageOverlayAlpha; } limb.BurnOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index f4c76ac1f..2e544f692 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -109,6 +109,7 @@ namespace Barotrauma private float wetTimer; private float dripParticleTimer; private float deadTimer; + private Color? randomColor; /// /// Note that different limbs can share the same deformations. @@ -120,6 +121,15 @@ namespace Barotrauma public List ActiveDeformations { get; set; } = new List(); public Sprite Sprite { get; protected set; } + public Sprite TintMask { get; protected set; } + + public Sprite HuskMask { get; protected set; } + public float TintHighlightThreshold { get; protected set; } + public float TintHighlightMultiplier { get; protected set; } + + private SpriteBatch.EffectWithParams tintEffectParams; + private SpriteBatch.EffectWithParams huskSpriteParams; + protected DeformableSprite _deformSprite; @@ -157,6 +167,12 @@ namespace Barotrauma } } + public Sprite GetActiveSprite(bool excludeConditionalSprites = true) + => excludeConditionalSprites ? (_deformSprite != null ? _deformSprite.Sprite : Sprite) + : ActiveSprite; + + public float DefaultSpriteDepth { get; private set; } + public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } @@ -273,6 +289,7 @@ namespace Barotrauma DecorativeSpriteGroups[groupID].Add(decorativeSprite); spriteAnimState.Add(decorativeSprite, new SpriteState()); } + TintMask = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -299,15 +316,42 @@ namespace Barotrauma Deformations.AddRange(deformations); NonConditionalDeformations.AddRange(deformations); break; + case "randomcolor": + randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandom(); + if (randomColor.HasValue) + { + Params.GetSprite().Color = randomColor.Value; + } + break; case "lightsource": LightSource = new LightSource(subElement, GetConditionalTarget()) { ParentBody = body, SpriteScale = Vector2.One * Scale * TextureScale }; + if (randomColor.HasValue) + { + LightSource.Color = new Color(randomColor.Value.R, randomColor.Value.G, randomColor.Value.B, LightSource.Color.A); + } InitialLightSourceColor = LightSource.Color; InitialLightSpriteAlpha = LightSource.OverrideLightSpriteAlpha; break; + case "tintmask": + string tintMaskPath = subElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(tintMaskPath)) + { + TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath)); + TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); + TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f); + } + break; + case "huskmask": + string huskMaskPath = subElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(huskMaskPath)) + { + HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); + } + break; } ISerializableEntity GetConditionalTarget() @@ -357,6 +401,7 @@ namespace Barotrauma return deformations; } } + DefaultSpriteDepth = GetActiveSprite()?.Depth ?? 0.0f; LightSource?.CheckConditionals(); } @@ -449,20 +494,20 @@ namespace Barotrauma /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// - private string GetSpritePath(string texturePath) + public static string GetSpritePath(string texturePath, CharacterInfo characterInfo) { string spritePath = texturePath; string spritePathWithTags = spritePath; - if (character.Info != null && character.IsHumanoid) + if (characterInfo != null) { - spritePath = spritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", character.Info.Race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString()); + spritePath = spritePath.Replace("[GENDER]", (characterInfo.Gender == Gender.Female) ? "female" : "male"); + spritePath = spritePath.Replace("[RACE]", characterInfo.Race.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[HEADID]", characterInfo.HeadSpriteId.ToString()); - if (character.Info.HeadSprite != null && character.Info.SpriteTags.Any()) + if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) { string tags = ""; - character.Info.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); + characterInfo.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), @@ -472,6 +517,13 @@ namespace Barotrauma return File.Exists(spritePathWithTags) ? spritePathWithTags : spritePath; } + + private string GetSpritePath(string texturePath) + { + if (!character.IsHumanoid) { return texturePath; } + return GetSpritePath(texturePath, character?.Info); + } + partial void LoadParamsProjSpecific() { bool isFlipped = dir == Direction.Left; @@ -538,8 +590,8 @@ namespace Barotrauma { foreach (ParticleEmitter emitter in character.DamageEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } + if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } ParticlePrefab overrideParticle = null; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { @@ -560,8 +612,8 @@ namespace Barotrauma foreach (ParticleEmitter emitter in character.BloodEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } + if (InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!InWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); } } @@ -585,7 +637,7 @@ namespace Barotrauma } } - if (inWater) + if (InWater) { wetTimer = 1.0f; } @@ -632,19 +684,38 @@ namespace Barotrauma RefreshDeformations(); } - public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) + public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } - Color color = new Color(spriteParams.Color.R / 255f * brightness, spriteParams.Color.G / 255f * brightness, spriteParams.Color.B / 255f * brightness, spriteParams.Color.A / 255f); + Color clr = spriteParams.Color; + if (!spriteParams.IgnoreTint) + { + clr = clr.Multiply(ragdoll.RagdollParams.Color); + if (character.Info != null) + { + clr = clr.Multiply(character.Info.SkinColor); + } + if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) + { + clr = Color.Lerp(clr, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f); + } + if (character.CharacterHealth.BodyTint.A > 0) + { + clr = Color.Lerp(clr, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f); + } + } + Color color = new Color((byte)(clr.R * brightness), (byte)(clr.G * brightness), (byte)(clr.B * brightness), clr.A); + Color blankColor = new Color(brightness, brightness, brightness, 1); if (deadTimer > 0) { color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); } color = overrideColor ?? color; + blankColor = overrideColor ?? blankColor; if (isSevered) { @@ -667,6 +738,8 @@ namespace Barotrauma OtherWearables.Any(w => w.HideLimb) || wearingItems.Any(w => w != null && w.HideLimb); + bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); + var activeSprite = ActiveSprite; if (type == LimbType.Head) { @@ -674,11 +747,12 @@ namespace Barotrauma } body.UpdateDrawPosition(); + float depthStep = 0.000001f; if (!hideLimb) { var deformSprite = DeformSprite; - if (deformSprite != null) + if (deformSprite != null && !disableDeformations) { if (ActiveDeformations.Any()) { @@ -698,7 +772,33 @@ namespace Barotrauma } else { + bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null; + if (useTintMask) + { + tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + tintEffectParams.Params ??= new Dictionary(); + var parameters = tintEffectParams.Params; + parameters["xBaseTexture"] = Sprite.Texture; + parameters["xTintMaskTexture"] = TintMask.Texture; + if (drawHuskSprite && HuskMask != null) + { + parameters["xCutoffTexture"] = HuskMask.Texture; + parameters["baseToCutoffSizeRatio"] = (float)Sprite.Texture.Width / (float)HuskMask.Texture.Width; + } + else + { + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + } + parameters["highlightThreshold"] = TintHighlightThreshold; + parameters["highlightMultiplier"] = TintHighlightMultiplier; + spriteBatch.SwapEffect(tintEffectParams); + } body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + if (useTintMask) + { + spriteBatch.SwapEffect(null); + } } // Handle non-exlusive, i.e. additional conditional sprites foreach (var conditionalSprite in ConditionalSprites) @@ -722,7 +822,7 @@ namespace Barotrauma } else { - body.Draw(spriteBatch, conditionalSprite.Sprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + body.Draw(spriteBatch, conditionalSprite.Sprite, color, depth: activeSprite.Depth - (depthStep * 50), Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } } } @@ -737,7 +837,7 @@ namespace Barotrauma new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), color * Math.Min(damageOverlayStrength, 1.0f), activeSprite.Origin, -body.DrawRotation, - Scale, spriteEffect, activeSprite.Depth - 0.0000015f); + Scale, spriteEffect, activeSprite.Depth - (depthStep * 90)); } foreach (var decorativeSprite in DecorativeSprites) { @@ -755,9 +855,8 @@ namespace Barotrauma Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, - depth: decorativeSprite.Sprite.Depth); + depth: activeSprite.Depth - (depthStep * 100)); } - float depthStep = 0.000001f; float step = depthStep; WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) @@ -770,23 +869,44 @@ namespace Barotrauma } if (onlyDrawable == null) { - if (HerpesSprite != null && !wearableTypesToHide.Contains(WearableType.Herpes)) + if (HerpesSprite != null && !wearableTypesToHide.Contains(WearableType.Herpes) && herpesStrength > 0) { - DrawWearable(HerpesSprite, depthStep, spriteBatch, color * Math.Min(herpesStrength / 10.0f, 1.0f), spriteEffect); + float alpha = Math.Min(herpesStrength * 2 / 100.0f, 1.0f); + DrawWearable(HerpesSprite, depthStep, spriteBatch, blankColor, alpha: alpha, spriteEffect); + depthStep += step; + } + if (drawHuskSprite) + { + bool useTintEffect = HuskMask != null && spriteBatch.GetCurrentEffect() is null; + if (useTintEffect) + { + huskSpriteParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + huskSpriteParams.Params ??= new Dictionary(); + var parameters = huskSpriteParams.Params; + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + spriteBatch.SwapEffect(huskSpriteParams); + } + DrawWearable(HuskSprite, depthStep, spriteBatch, color, alpha: color.A / 255f, spriteEffect); + if (useTintEffect) + { + spriteBatch.SwapEffect(null); + } depthStep += step; } foreach (WearableSprite wearable in OtherWearables) { + if (wearable.Type == WearableType.Husk) { continue; } if (wearableTypesToHide.Contains(wearable.Type)) { continue; } - DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); + DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } } foreach (WearableSprite wearable in WearingItems) { - if (onlyDrawable != null && onlyDrawable != wearable) continue; - DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); + if (onlyDrawable != null && onlyDrawable != wearable && wearable.CanBeHiddenByOtherWearables) { continue; } + DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } @@ -936,7 +1056,7 @@ namespace Barotrauma } } - private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, SpriteEffects spriteEffect) + private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) { var sprite = ActiveSprite; if (wearable.InheritSourceRect) @@ -955,7 +1075,7 @@ namespace Barotrauma } } - Vector2 origin = wearable.Sprite.Origin; + Vector2 origin; if (wearable.InheritOrigin) { origin = sprite.Origin; @@ -986,24 +1106,49 @@ namespace Barotrauma Color wearableColor = Color.White; if (wearableItemComponent != null) { - // Draw outer cloths on top of inner cloths. + // Draw outer clothes on top of inner clothes. if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)) { depth -= depthStep; } if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.Bag)) { - depth -= depthStep * 2; + depth -= depthStep * 4; } wearableColor = wearableItemComponent.Item.GetSpriteColor(); } - float textureScale = wearable.InheritTextureScale ? TextureScale : wearable.Scale; - - wearable.Sprite.Draw(spriteBatch, - new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - new Color((color.R * wearableColor.R) / (255.0f * 255.0f), (color.G * wearableColor.G) / (255.0f * 255.0f), (color.B * wearableColor.B) / (255.0f * 255.0f)) * ((color.A * wearableColor.A) / (255.0f * 255.0f)), - origin, -body.DrawRotation, - Scale * textureScale, spriteEffect, depth); + else if (character.Info != null) + { + if (wearable.Type == WearableType.Hair) + { + wearableColor = character.Info.HairColor; + } + else if (wearable.Type == WearableType.Beard || wearable.Type == WearableType.Moustache) + { + wearableColor = character.Info.FacialHairColor; + } + } + float scale = wearable.Scale; + if (wearable.InheritScale) + { + if (!wearable.IgnoreTextureScale) + { + scale *= TextureScale; + } + if (!wearable.IgnoreLimbScale) + { + scale *= Params.Scale; + } + if (!wearable.IgnoreRagdollScale) + { + scale *= ragdoll.RagdollParams.LimbScale; + } + } + float rotation = -body.DrawRotation - wearable.Rotation * Dir; + float finalAlpha = alpha * wearableColor.A; + Color finalColor = color.Multiply(wearableColor); + finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); + wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); } private WearableSprite GetWearableSprite(WearableType type, bool random = false) @@ -1054,6 +1199,9 @@ namespace Barotrauma HerpesSprite?.Sprite.Remove(); HerpesSprite = null; + + TintMask?.Remove(); + TintMask = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 62218ed85..517fcd516 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -418,14 +418,14 @@ namespace Barotrauma ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N", (option2) => { - if (option2.ToLower() == "y") { GameMain.SubEditorScreen.AutoHull(); } + if (option2.ToLowerInvariant() == "y") { GameMain.SubEditorScreen.AutoHull(); } }); }); } else { ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N", - (option) => { if (option.ToLower() == "y") GameMain.SubEditorScreen.AutoHull(); }); + (option) => { if (option.ToLowerInvariant() == "y") GameMain.SubEditorScreen.AutoHull(); }); } })); @@ -476,6 +476,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { NewMessage("WARNING: Switching directly from the submarine editor to the game view may cause bugs and crashes. Use with caution.", Color.Orange); + Entity.Spawner ??= new EntitySpawner(); } GameMain.GameScreen.Select(); })); @@ -488,6 +489,8 @@ namespace Barotrauma Submarine.MainSub = Submarine.Load(subInfo, true); } GameMain.SubEditorScreen.Select(enableAutoSave: Screen.Selected != GameMain.GameScreen); + Entity.Spawner?.Remove(); + Entity.Spawner = null; }, isCheat: true)); commands.Add(new Command("editparticles|particleeditor", "editparticles/particleeditor: Switch to the Particle Editor to edit particle effects.", (string[] args) => @@ -608,7 +611,7 @@ namespace Barotrauma ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({Keybinds.Count} keybind(s) assigned) Y/N", (option2) => { - if (option2.ToLower() != "y") + if (option2.ToLowerInvariant() != "y") { NewMessage("Aborted.", GUI.Style.Red); return; @@ -689,9 +692,18 @@ namespace Barotrauma AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); + AssignRelayToServer("givetalent", true); + AssignRelayToServer("unlocktalents", true); + AssignRelayToServer("giveexperience", true); + AssignOnExecute("control", (string[] args) => { - if (args.Length < 1) return; + if (args.Length < 1) { return; } + if (GameMain.NetworkMember != null) + { + GameMain.Client?.SendConsoleCommand("control " + string.Join(' ', args[0])); + return; + } var character = FindMatchingCharacter(args, true); if (character != null) { @@ -1096,9 +1108,35 @@ namespace Barotrauma commands.Add(new Command("load|loadsub", "load [submarine name]: Load a submarine.", (string[] args) => { - if (args.Length == 0) return; - SubmarineInfo subInfo = new SubmarineInfo(string.Join(" ", args)); + if (args.Length == 0) { return; } + + if (GameMain.GameSession != null) + { + ThrowError("The loadsub command cannot be used when a round is running. You should probably be using spawnsub instead."); + return; + } + + string name = string.Join(" ", args); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => name.Equals(s.Name, StringComparison.OrdinalIgnoreCase)); + if (subInfo == null) + { + string path = Path.Combine(SubmarineInfo.SavePath, name); + if (!File.Exists(path)) + { + ThrowError($"Could not find a submarine with the name \"{name}\" or in the path {path}."); + return; + } + subInfo = new SubmarineInfo(path); + } + Submarine.Load(subInfo, true); + }, + () => + { + return new string[][] + { + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() + }; })); commands.Add(new Command("cleansub", "", (string[] args) => @@ -1317,11 +1355,13 @@ namespace Barotrauma continue; } + float avgOutCondition = (deconstructItem.OutConditionMin + deconstructItem.OutConditionMax) / 2; + int? deconstructProductPrice = targetItem.GetMinPrice(); if (deconstructProductPrice.HasValue) { if (!deconstructProductCost.HasValue) { deconstructProductCost = 0; } - deconstructProductCost += (int)(deconstructProductPrice * deconstructItem.OutCondition); + deconstructProductCost += (int)(deconstructProductPrice * avgOutCondition); } if (fabricationRecipe != null) @@ -1331,9 +1371,9 @@ namespace Barotrauma { NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); } - else if (ingredient.UseCondition && ingredient.MinCondition < deconstructItem.OutCondition) + else if (ingredient.UseCondition && ingredient.MinCondition < avgOutCondition) { - NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); + NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(avgOutCondition * 100)}%)", Color.Red); } } } @@ -1692,13 +1732,14 @@ namespace Barotrauma //check missing mission texts foreach (var missionPrefab in MissionPrefab.List) { - string nameIdentifier = "missionname." + missionPrefab.Identifier; + string missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeString("textidentifier", string.Empty)); + string nameIdentifier = "missionname." + missionId; if (!tags[language].Contains(nameIdentifier)) { if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "missiondescription." + missionPrefab.Identifier; + string descriptionIdentifier = "missiondescription." + missionId; if (!tags[language].Contains(descriptionIdentifier)) { if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } @@ -1708,6 +1749,7 @@ namespace Barotrauma foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { + if (sub.Type != SubmarineType.Player) { continue; } string nameIdentifier = "submarine.name." + sub.Name.ToLowerInvariant(); if (!tags[language].Contains(nameIdentifier)) { @@ -1724,14 +1766,23 @@ namespace Barotrauma foreach (AfflictionPrefab affliction in AfflictionPrefab.List) { - string nameIdentifier = "afflictionname." + affliction.Identifier; + if (affliction.ShowIconThreshold > affliction.MaxStrength && + affliction.ShowIconToOthersThreshold > affliction.MaxStrength && + affliction.ShowInHealthScannerThreshold > affliction.MaxStrength) + { + //hidden affliction, no need for localization + continue; + } + + string afflictionId = affliction.TranslationOverride ?? affliction.Identifier; + string nameIdentifier = "afflictionname." + afflictionId; if (!tags[language].Contains(nameIdentifier)) { if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "afflictiondescription." + affliction.Identifier; + string descriptionIdentifier = "afflictiondescription." + afflictionId; if (!tags[language].Contains(descriptionIdentifier)) { if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } @@ -1739,6 +1790,29 @@ namespace Barotrauma } } + foreach (var talentTree in TalentTree.JobTalentTrees) + { + foreach (var talentSubTree in talentTree.Value.TalentSubTrees) + { + string nameIdentifier = "talenttree." + talentSubTree.Identifier; + if (!tags[language].Contains(nameIdentifier)) + { + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + missingTags[nameIdentifier].Add(language); + } + } + } + + foreach (var talent in TalentPrefab.TalentPrefabs) + { + string nameIdentifier = "talentname." + talent.Identifier; + if (!tags[language].Contains(nameIdentifier)) + { + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + missingTags[nameIdentifier].Add(language); + } + } + //check missing entity names foreach (MapEntityPrefab me in MapEntityPrefab.List) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index 55191b4b4..d5161fc4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -22,6 +22,7 @@ namespace Barotrauma public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); ushort targetItemCount = msg.ReadUInt16(); for (int i = 0; i < targetItemCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..b943081b2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,32 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + existingTargets.Clear(); + spawnedTargets.Clear(); + allTargets.Clear(); + ushort existingTargetsCount = msg.ReadUInt16(); + for (int i = 0; i < existingTargetsCount; i++) + { + ushort targetId = msg.ReadUInt16(); + if (targetId == Entity.NullEntityID) { continue; } + Entity target = Entity.FindEntityByID(targetId); + if (target == null) { continue; } + existingTargets.Add(target); + allTargets.Add(target); + } + ushort spawnedTargetsCount = msg.ReadUInt16(); + for (int i = 0; i < spawnedTargetsCount; i++) + { + var enemy = Character.ReadSpawnData(msg); + existingTargets.Add(enemy); + allTargets.Add(enemy); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs deleted file mode 100644 index 584e13d3e..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class BeaconMission : Mission - { - public override void ClientReadInitial(IReadMessage msg) - { - return; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 97bcbc4d5..23b228922 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -25,6 +25,7 @@ namespace Barotrauma } public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); items.Clear(); ushort itemCount = msg.ReadUInt16(); for (int i = 0; i < itemCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index fc8cafcd4..9fb2b490b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -20,10 +20,5 @@ namespace Barotrauma return descriptions[GameMain.Client.Character.TeamID == CharacterTeamType.Team1 ? 1 : 2]; } } - - public override void ClientReadInitial(IReadMessage msg) - { - //do nothing - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs index 5fbdc15a5..230570e5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); + byte characterCount = msg.ReadByte(); for (int i = 0; i < characterCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index 61e23e9a5..3ce80fd05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -12,6 +12,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); byte caveCount = msg.ReadByte(); for (int i = 0; i < caveCount; i++) { @@ -29,7 +30,7 @@ namespace Barotrauma } } - for (int i = 0; i < ResourceClusters.Count; i++) + for (int i = 0; i < resourceClusters.Count; i++) { var amount = msg.ReadByte(); var rotation = msg.ReadSingle(); @@ -41,20 +42,20 @@ namespace Barotrauma h.AttachToWall(); item.Rotation = rotation; } - if (SpawnedResources.TryGetValue(item.Prefab.Identifier, out var resources)) + if (spawnedResources.TryGetValue(item.Prefab.Identifier, out var resources)) { resources.Add(item); } else { - SpawnedResources.Add(item.Prefab.Identifier, new List() { item }); + spawnedResources.Add(item.Prefab.Identifier, new List() { item }); } } } CalculateMissionClusterPositions(); - for(int i = 0; i < ResourceClusters.Count; i++) + for(int i = 0; i < resourceClusters.Count; i++) { var identifier = msg.ReadString(); var count = msg.ReadByte(); @@ -66,7 +67,7 @@ namespace Barotrauma if (!(entity is Item item)) { continue; } resources[j] = item; } - RelevantLevelResources.Add(identifier, resources); + relevantLevelResources.Add(identifier, resources); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 92d36b5fe..e8b3064d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; namespace Barotrauma { @@ -14,6 +15,10 @@ namespace Barotrauma get { return shownMessages; } } + public bool DisplayTargetHudIcons => Prefab.DisplayTargetHudIcons; + + public virtual IEnumerable HudIconTargets => Enumerable.Empty(); + public Color GetDifficultyColor() { int v = Difficulty ?? MissionPrefab.MinDifficulty; @@ -92,11 +97,14 @@ namespace Barotrauma }; } - public void ClientRead(IReadMessage msg) + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); } - public abstract void ClientReadInitial(IReadMessage msg); + public virtual void ClientReadInitial(IReadMessage msg) + { + state = msg.ReadInt16(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 5bc64d50f..e3c6f8633 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -18,13 +18,54 @@ namespace Barotrauma private set; } + public bool DisplayTargetHudIcons + { + get; + private set; + } + + public float HudIconMaxDistance + { + get; + private set; + } + + public Sprite HudIcon + { + get + { + return hudIcon ?? Icon; + } + } + + public Color HudIconColor + { + get + { + return hudIconColor ?? IconColor; + } + } + + private Sprite hudIcon; + private Color? hudIconColor; + partial void InitProjSpecific(XElement element) { + DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); + HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); foreach (XElement subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { continue; } - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); + string name = subElement.Name.ToString(); + if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + { + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + } + else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) + { + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs index 162968040..e5aab9f41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs @@ -6,6 +6,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); byte monsterCount = msg.ReadByte(); for (int i = 0; i < monsterCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs index 9de9c32c0..dcc6ee187 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -8,6 +8,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); byte selectedCaveIndex = msg.ReadByte(); nestPosition = new Vector2( msg.ReadSingle(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs index fa71317bc..d111e664c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs @@ -6,6 +6,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); // duplicate code from escortmission, should possibly be combined, though additional loot items might be added so maybe not byte characterCount = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 8755b9d40..6479ddb90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -7,6 +7,7 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + base.ClientReadInitial(msg); bool usedExistingItem = msg.ReadBoolean(); if (usedExistingItem) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..93dadacc4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs @@ -0,0 +1,67 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + public override IEnumerable HudIconTargets + { + get + { + if (State == 0) + { + return scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key); + } + else + { + return Enumerable.Empty(); + } + } + } + + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + startingItems.Clear(); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + startingItems.Add(Item.ReadSpawnData(msg)); + } + if (startingItems.Contains(null)) + { + throw new Exception($"Error in ScanMission.ClientReadInitial: item list contains null (mission: {Prefab.Identifier})"); + } + if (startingItems.Count != itemCount) + { + throw new Exception($"Error in ScanMission.ClientReadInitial: item count does not match the server count ({itemCount} != {startingItems.Count}, mission: {Prefab.Identifier})"); + } + scanners.Clear(); + GetScanners(); + ClientReadScanTargetStatus(msg); + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + ClientReadScanTargetStatus(msg); + } + + private void ClientReadScanTargetStatus(IReadMessage msg) + { + scanTargets.Clear(); + byte targetsToScan = msg.ReadByte(); + for (int i = 0; i < targetsToScan; i++) + { + ushort id = msg.ReadUInt16(); + bool scanned = msg.ReadBoolean(); + Entity entity = Entity.FindEntityByID(id); + scanTargets.Add(entity as WayPoint, scanned); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 57e963b91..18c65a9f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -130,6 +130,7 @@ namespace Barotrauma public static GUIStyle Style; private static Texture2D t; + public static Texture2D WhiteTexture => t; private static Sprite[] MouseCursorSprites => Style.CursorSprite; private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; @@ -163,6 +164,7 @@ namespace Barotrauma public static ScalableFont SubHeadingFont => Style?.SubHeadingFont; public static ScalableFont DigitalFont => Style?.DigitalFont; public static ScalableFont HotkeyFont => Style?.HotkeyFont; + public static ScalableFont MonospacedFont => Style?.MonospacedFont; public static ScalableFont CJKFont { get; private set; } @@ -306,7 +308,7 @@ namespace Barotrauma }); SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); - arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(392, 393, 49, 45), new Vector2(0.5f, 0.5f)); + arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); BrokenIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(898, 386, 123, 123), new Vector2(0.5f, 0.5f)); } @@ -672,6 +674,12 @@ namespace Barotrauma spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); + if (GameMain.GameSession?.CrewManager is { DraggedOrder: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) + { + float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y); + orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale); + } + var sprite = MouseCursorSprites[(int)MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); @@ -855,11 +863,10 @@ namespace Barotrauma int index = 0; if (updateList.Count > 0) { - index = updateList.Count - 1; - while (updateList[index].UpdateOrder > item.UpdateOrder) + index = updateList.Count; + while (index > 0 && updateList[index-1].UpdateOrder > item.UpdateOrder) { index--; - if (index == 0) { break; } } } if (!updateListSet.Contains(item)) @@ -927,13 +934,14 @@ namespace Barotrauma GUIComponent prevMouseOn = MouseOn; MouseOn = null; int inventoryIndex = -1; - - if (Inventory.IsMouseOnInventory()) + + Inventory.RefreshMouseOnInventory(); + if (Inventory.IsMouseOnInventory) { inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); } - if (!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld())) { for (var i = updateList.Count - 1; i > inventoryIndex; i--) { @@ -941,10 +949,9 @@ namespace Barotrauma if (!c.CanBeFocused) { continue; } if (c.MouseRect.Contains(PlayerInput.MousePosition)) { - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn || prevMouseOn == null) { MouseOn = c; - var sakdjfnsjkd = c.MouseRect; } break; } @@ -958,7 +965,6 @@ namespace Barotrauma MouseCursor = UpdateMouseCursorState(MouseOn); return MouseOn; } - } private static CursorState UpdateMouseCursorState(GUIComponent c) @@ -1050,23 +1056,26 @@ namespace Barotrauma { if (listBox.DraggedElement != null) { return CursorState.Dragging; } if (listBox.CanDragElements) { return CursorState.Move; } - - var hoverParent = c; - while (true) + + if (listBox.HoverCursor != CursorState.Default) { - if (hoverParent == parent || hoverParent == null) { break; } - if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } - hoverParent = hoverParent.Parent; + var hoverParent = c; + while (true) + { + if (hoverParent == parent || hoverParent == null) { break; } + if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } + hoverParent = hoverParent.Parent; + } } } - + if (parent != null && parent.CanBeFocused) { if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; } } } - - if (Inventory.IsMouseOnInventory()) { return Inventory.GetInventoryMouseCursor(); } + + if (Inventory.IsMouseOnInventory) { return Inventory.GetInventoryMouseCursor(); } var character = Character.Controlled; // ReSharper disable once InvertIf @@ -1343,7 +1352,7 @@ namespace Barotrauma /// Should the indicator move based on the camera position? /// Override the distance-based alpha value with the specified alpha value - public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Vector2 visibleRange, Sprite sprite, in Color color, + public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Range visibleRange, Sprite sprite, in Color color, bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { Vector2 diff = worldPosition - cam.WorldViewCenter; @@ -1351,9 +1360,9 @@ namespace Barotrauma float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; - if (overrideAlpha.HasValue || (dist > visibleRange.X && dist < visibleRange.Y)) + if (overrideAlpha.HasValue || (dist > visibleRange.Start && dist < visibleRange.End)) { - float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.X) / 100.0f, 1.0f - ((dist - visibleRange.Y + 100f) / 100.0f), 1.0f); + float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.Start) / 100.0f, 1.0f - ((dist - visibleRange.End + 100f) / 100.0f), 1.0f); Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); if (!createOffset) @@ -1417,7 +1426,7 @@ namespace Barotrauma public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { - DrawIndicator(spriteBatch, worldPosition, cam, new Vector2(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha); + DrawIndicator(spriteBatch, worldPosition, cam, new Range(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha); } public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) @@ -1520,6 +1529,11 @@ namespace Barotrauma } } + public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f) + { + DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth); + } + public static void DrawFilledRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, float depth = 0.0f) { if (size.X < 0) @@ -2433,14 +2447,43 @@ namespace Barotrauma { if (messages.Any(msg => msg.Text == message)) { return; } messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? LargeFont)); - if (playSound) SoundPlayer.PlayUISound(GUISoundType.UIMessage); + if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } public static void AddMessage(string message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1) { Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId); - messages.Add(new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, LargeFont, sub: sub)); - if (playSound) SoundPlayer.PlayUISound(soundType); + + var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, Font, sub: sub); + if (playSound) { SoundPlayer.PlayUISound(soundType); } + bool overlapFound = true; + int tries = 0; + while (overlapFound) + { + overlapFound = false; + foreach (var otherMessage in messages) + { + float xDiff = otherMessage.Pos.X - newMessage.Pos.X; + if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } + float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; + if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } + Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); + if (moveDir.LengthSquared() > 0.0001f) + { + moveDir = Vector2.Normalize(moveDir); + } + else + { + moveDir = Rand.Vector(1.0f); + } + moveDir.Y = -Math.Abs(moveDir.Y); + newMessage.Pos -= Vector2.UnitY * 10; + } + tries++; + if (tries > 20) { break; } + } + + messages.Add(newMessage); } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index ecc75885a..34e99bc03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -52,13 +52,13 @@ namespace Barotrauma public GUIComponent GetChild(int index) { - if (index < 0 || index >= CountChildren) return null; + if (index < 0 || index >= CountChildren) { return null; } return RectTransform.GetChild(index).GUIComponent; } public int GetChildIndex(GUIComponent child) { - if (child == null) return -1; + if (child == null) { return -1; } return RectTransform.GetChildIndex(child.RectTransform); } @@ -66,7 +66,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) return child; + if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) { return child; } } return null; } @@ -175,6 +175,8 @@ namespace Barotrauma public bool GlowOnSelect { get; set; } + public Vector2 UVOffset { get; set; } + private CoroutineHandle pulsateCoroutine; protected Color flashColor; @@ -256,9 +258,9 @@ namespace Barotrauma protected Rectangle ClampRect(Rectangle r) { - if (Parent == null || !ClampMouseRectToParent) return r; + if (Parent == null || !ClampMouseRectToParent) { return r; } Rectangle parentRect = Parent.ClampRect(Parent.Rect); - if (parentRect.Width <= 0 || parentRect.Height <= 0) return Rectangle.Empty; + if (parentRect.Width <= 0 || parentRect.Height <= 0) { return Rectangle.Empty; } if (parentRect.X > r.X) { int diff = parentRect.X - r.X; @@ -281,7 +283,7 @@ namespace Barotrauma int diff = (r.Y + r.Height) - (parentRect.Y + parentRect.Height); r.Height -= diff; } - if (r.Width <= 0 || r.Height <= 0) return Rectangle.Empty; + if (r.Width <= 0 || r.Height <= 0) { return Rectangle.Empty; } return r; } @@ -295,7 +297,7 @@ namespace Barotrauma { get { - if (!CanBeFocused) return Rectangle.Empty; + if (!CanBeFocused) { return Rectangle.Empty; } return ClampMouseRectToParent ? ClampRect(Rect) : Rect; } } @@ -431,7 +433,7 @@ namespace Barotrauma #region Updating public virtual void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { - if (!Visible) return; + if (!Visible) { return; } UpdateOrder = order; GUI.AddToUpdateList(this); @@ -463,7 +465,7 @@ namespace Barotrauma /// public void UpdateManually(float deltaTime, bool alsoChildren = false, bool recursive = true) { - if (!Visible) return; + if (!Visible) { return; } AutoUpdate = false; Update(deltaTime); @@ -475,7 +477,7 @@ namespace Barotrauma protected virtual void Update(float deltaTime) { - if (!Visible) return; + if (!Visible) { return; } if (CanBeFocused && OnSecondaryClicked != null) { @@ -555,7 +557,7 @@ namespace Barotrauma /// public virtual void DrawManually(SpriteBatch spriteBatch, bool alsoChildren = false, bool recursive = true) { - if (!Visible) return; + if (!Visible) { return; } AutoDraw = false; Draw(spriteBatch); @@ -598,7 +600,7 @@ namespace Barotrauma protected virtual void Draw(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible) { return; } var rect = Rect; GetBlendedColor(GetColor(State), ref _currentColor); @@ -653,7 +655,7 @@ namespace Barotrauma ? MathUtils.InverseLerp(0, SpriteCrossFadeTime, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : 0; if (alphaMultiplier > 0) { - uiSprite.Draw(spriteBatch, rect, previousColor * alphaMultiplier, SpriteEffects); + uiSprite.Draw(spriteBatch, rect, previousColor * alphaMultiplier, SpriteEffects, uvOffset: UVOffset); } } } @@ -667,7 +669,11 @@ namespace Barotrauma ? MathUtils.InverseLerp(SpriteCrossFadeTime, 0, ToolBox.GetEasing(uiSprite.TransitionMode, spriteFadeTimer)) : (_currentColor.A / 255.0f); if (alphaMultiplier > 0) { - uiSprite.Draw(spriteBatch, rect, _currentColor * alphaMultiplier, SpriteEffects); + // * (rect.Location.Y - PlayerInput.MousePosition.Y) / rect.Height + Vector2 offset = new Vector2( + MathUtils.PositiveModulo((int)-UVOffset.X, uiSprite.Sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-UVOffset.Y, uiSprite.Sprite.SourceRect.Height)); + uiSprite.Draw(spriteBatch, rect, _currentColor * alphaMultiplier, SpriteEffects, uvOffset: offset); } } } @@ -708,7 +714,7 @@ namespace Barotrauma /// public void DrawToolTip(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible) { return; } DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); } @@ -1048,7 +1054,7 @@ namespace Barotrauma { case "language": string[] languages = element.GetAttributeStringArray(attribute.Name.ToString(), new string[0]); - if (!languages.Any(l => GameMain.Config.Language.ToLower() == l.ToLower())) { return false; } + if (!languages.Any(l => GameMain.Config.Language.Equals(l, StringComparison.OrdinalIgnoreCase))) { return false; } break; case "gameversion": var version = new Version(attribute.Value); @@ -1213,8 +1219,7 @@ namespace Barotrauma private static GUIImage LoadGUIImage(XElement element, RectTransform parent) { - Sprite sprite = null; - + Sprite sprite; string url = element.GetAttributeString("url", ""); if (!string.IsNullOrEmpty(url)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index d1560e7ed..6c2780430 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -23,6 +23,8 @@ namespace Barotrauma private bool selectMultiple; public bool Dropped { get; set; } + + public bool AllowNonText { get; set; } public object SelectedItemData { @@ -318,9 +320,9 @@ namespace Barotrauma if (textBlock == null) { textBlock = component.GetChild(); - if (textBlock == null) return false; + if (textBlock is null && !AllowNonText) { return false; } } - button.Text = textBlock.Text; + button.Text = textBlock?.Text ?? ""; } Dropped = false; // TODO: OnSelected can be called multiple times and when it shouldn't be called -> turn into an event so that nobody else can call it. diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs index 0e5d5ca42..4966580c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIFrame.cs @@ -5,8 +5,8 @@ using System.Linq; namespace Barotrauma { public class GUIFrame : GUIComponent - { - public int OutlineThickness { get; set; } + { + public float OutlineThickness { get; set; } public GUIFrame(RectTransform rectT, string style = "", Color? color = null) : base(style, rectT) { @@ -28,7 +28,7 @@ namespace Barotrauma if (OutlineColor != Color.Transparent) { - GUI.DrawRectangle(spriteBatch, Rect, OutlineColor * (OutlineColor.A/255.0f), false, thickness: OutlineThickness); + GUI.DrawRectangle(spriteBatch, Rect, OutlineColor, false, thickness: OutlineThickness); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 086cc25e7..e7be6a3ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -174,7 +174,7 @@ namespace Barotrauma if (BlendState != null) { spriteBatch.End(); - spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState); + spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } if (style != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 9761bf213..2a010e707 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -33,7 +33,7 @@ namespace Barotrauma public GUIFrame Content { get; private set; } public GUIScrollBar ScrollBar { get; private set; } - private Dictionary childVisible = new Dictionary(); + private readonly Dictionary childVisible = new Dictionary(); private int totalSize; private bool childrenNeedsRecalculation; @@ -224,7 +224,7 @@ namespace Barotrauma { if (value == false && canDragElements && draggedElement != null) { - draggedElement = null; + DraggedElement = null; } canDragElements = value; } @@ -233,11 +233,29 @@ namespace Barotrauma private GUIComponent draggedElement; private Rectangle draggedReferenceRectangle; private Point draggedReferenceOffset; + public bool HasDraggedElementIndexChanged { get; private set; } - public GUIComponent DraggedElement => draggedElement; + public GUIComponent DraggedElement + { + get + { + return draggedElement; + } + set + { + if (value == draggedElement) { return; } + draggedElement = value; + HasDraggedElementIndexChanged = false; + } + } private readonly bool isHorizontal; + /// + /// Setting this to true and CanBeFocused to false allows the list background to be unfocusable while the elements can still be interacted with. + /// + public bool CanInteractWhenUnfocusable { get; set; } = false; + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { @@ -472,7 +490,7 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { OnRearranged?.Invoke(this, draggedElement.UserData); - draggedElement = null; + DraggedElement = null; RepositionChildren(); } else @@ -518,6 +536,7 @@ namespace Barotrauma if (currIndex != index) { draggedElement.RectTransform.RepositionChildInHierarchy(currIndex); + HasDraggedElementIndexChanged = true; } return; @@ -556,7 +575,7 @@ namespace Barotrauma if (child == null || !child.Visible) { continue; } // selecting - if (Enabled && CanBeFocused && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) + if (Enabled && (CanBeFocused || CanInteractWhenUnfocusable) && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) { child.State = ComponentState.Hover; @@ -577,7 +596,7 @@ namespace Barotrauma if (CanDragElements && PlayerInput.PrimaryMouseButtonDown() && GUI.MouseOn == child) { - draggedElement = child; + DraggedElement = child; draggedReferenceRectangle = child.Rect; draggedReferenceOffset = child.RectTransform.AbsoluteOffset; } @@ -608,7 +627,7 @@ namespace Barotrauma { if (child == Content || child == ScrollBar || child == ContentBackground) { continue; } child.AddToGUIUpdateList(ignoreChildren, order); - } + } } foreach (GUIComponent child in Content.Children) @@ -637,7 +656,7 @@ namespace Barotrauma OnAddedToGUIUpdateList?.Invoke(this); return; } - + int lastVisible = 0; for (int i = 0; i < Content.CountChildren; i++) { @@ -681,6 +700,8 @@ namespace Barotrauma } } + public void ForceUpdate() => Update((float)Timing.Step); + protected override void Update(float deltaTime) { if (!Visible) { return; } @@ -750,7 +771,7 @@ namespace Barotrauma } } - if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && AllowMouseWheelScroll && PlayerInput.ScrollWheelSpeed != 0) + if (PlayerInput.ScrollWheelSpeed != 0 && AllowMouseWheelScroll && (FindScrollableParentListBox(GUI.MouseOn) == this || GUI.IsMouseOn(ScrollBar))) { if (SmoothScroll) { @@ -773,7 +794,6 @@ namespace Barotrauma ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; } } - ScrollBar.Enabled = ScrollBarEnabled && BarSize < 1.0f; if (AutoHideScrollBar) @@ -785,6 +805,13 @@ namespace Barotrauma UpdateDimensions(); } } + + private static GUIListBox FindScrollableParentListBox(GUIComponent target) + { + if (target is GUIListBox listBox && listBox.ScrollBarEnabled && listBox.BarSize < 1.0f) { return listBox; } + if (target?.Parent == null) { return null; } + return FindScrollableParentListBox(target.Parent); + } public void SelectNext(bool force = false, bool autoScroll = true, bool takeKeyBoardFocus = false) { @@ -982,7 +1009,7 @@ namespace Barotrauma if (child == null) { return; } child.RectTransform.Parent = null; if (selected.Contains(child)) { selected.Remove(child); } - if (draggedElement == child) { draggedElement = null; } + if (draggedElement == child) { DraggedElement = null; } UpdateScrollBarSize(); } @@ -999,7 +1026,6 @@ namespace Barotrauma ContentBackground.DrawManually(spriteBatch, alsoChildren: false); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; - RasterizerState prevRasterizerState = spriteBatch.GraphicsDevice.RasterizerState; if (HideChildrenOutsideFrame) { spriteBatch.End(); @@ -1027,7 +1053,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } if (ScrollBarVisible) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 0c373187d..33f42b55d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -79,14 +79,14 @@ namespace Barotrauma new Rectangle( sliderArea.X + (int)sliceBorderSizes.X, sliderArea.Y, - (int)((sliderArea.Width - sliceBorderSizes.X - sliceBorderSizes.Z) * fillAmount), + (int)Math.Round((sliderArea.Width - sliceBorderSizes.X - sliceBorderSizes.Z) * fillAmount), sliderArea.Height) : new Rectangle( sliderArea.X, - (int)(sliderArea.Bottom - (sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount - sliceBorderSizes.W), + (int)Math.Round(sliderArea.Bottom - (sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount - sliceBorderSizes.W), sliderArea.Width, - (int)((sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount)); + (int)Math.Round((sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount)); sliderRect.Width = Math.Max(sliderRect.Width, 1); sliderRect.Height = Math.Max(sliderRect.Height, 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs new file mode 100644 index 000000000..07119e38a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + public class GUIScissorComponent: GUIComponent + { + public GUIComponent Content; + + public GUIScissorComponent(RectTransform rectT) : base(null, rectT) + { + Content = new GUIFrame(new RectTransform(Vector2.One, rectT), style: null) + { + CanBeFocused = false + }; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + foreach (GUIComponent child in Children) + { + if (child == Content) { continue; } + throw new InvalidOperationException($"Children were found in {nameof(GUIScissorComponent)}, Add them to {nameof(GUIScissorComponent)}.{nameof(Content)} instead."); + } + + ClampChildMouseRects(Content); + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (!Visible) { return; } + + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + RasterizerState prevRasterizerState = spriteBatch.GraphicsDevice.RasterizerState; + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Rect); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + + foreach (GUIComponent child in Content.Children) + { + if (!child.Visible) { continue; } + child.DrawManually(spriteBatch, alsoChildren: true, recursive: true); + } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); + } + + private void ClampChildMouseRects(GUIComponent child) + { + child.ClampMouseRectToParent = true; + + if (child is GUIListBox) { return; } + + foreach (GUIComponent grandChild in child.Children) + { + ClampChildMouseRects(grandChild); + } + } + + public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) + { + if (!Visible) { return; } + + UpdateOrder = order; + GUI.AddToUpdateList(this); + + if (ignoreChildren) + { + OnAddedToGUIUpdateList?.Invoke(this); + return; + } + + foreach (GUIComponent child in Content.Children) + { + if (!child.Visible) { continue; } + child.AddToGUIUpdateList(false, order); + } + OnAddedToGUIUpdateList?.Invoke(this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 169b4d758..6c6224e8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -11,7 +11,7 @@ namespace Barotrauma { private Dictionary componentStyles; - private XElement configElement; + private readonly XElement configElement; private GraphicsDevice graphicsDevice; @@ -25,6 +25,7 @@ namespace Barotrauma public ScalableFont SubHeadingFont { get; private set; } public ScalableFont DigitalFont { get; private set; } public ScalableFont HotkeyFont { get; private set; } + public ScalableFont MonospacedFont { get; private set; } public Dictionary ForceFontUpperCase { @@ -40,12 +41,20 @@ namespace Barotrauma public SpriteSheet SavingIndicator { get; private set; } public UISprite UIGlow { get; private set; } + + public UISprite PingCircle { get; private set; } + public UISprite UIGlowCircular { get; private set; } + public UISprite UIGlowSolidCircular { get; private set; } + public UISprite UIThermalGlow { get; private set; } + public UISprite ButtonPulse { get; private set; } public SpriteSheet FocusIndicator { get; private set; } + public UISprite IconOverflowIndicator { get; private set; } + /// /// General green color used for elements whose colors are set from code /// @@ -82,6 +91,12 @@ namespace Barotrauma public Color TextColorDark { get; private set; } = Color.Black * 0.9f; public Color TextColorDim { get; private set; } = Color.White * 0.6f; + public Color ItemQualityColorPoor { get; private set; } = Color.DarkRed; + public Color ItemQualityColorNormal { get; private set; } = Color.Gray; + public Color ItemQualityColorGood { get; private set; } = Color.LightGreen; + public Color ItemQualityColorExcellent { get; private set; } = Color.LightBlue; + public Color ItemQualityColorMasterwork { get; private set; } = Color.MediumPurple; + public Color ColorReputationVeryLow { get; private set; } = Color.Red; public Color ColorReputationLow { get; private set; } = Color.Orange; public Color ColorReputationNeutral { get; private set; } = Color.White * 0.8f; @@ -235,6 +250,9 @@ namespace Barotrauma case "uiglow": UIGlow = new UISprite(subElement); break; + case "pingcircle": + PingCircle = new UISprite(subElement); + break; case "radiation": RadiationSprite = new UISprite(subElement); break; @@ -244,9 +262,18 @@ namespace Barotrauma case "uiglowcircular": UIGlowCircular = new UISprite(subElement); break; + case "uiglowsolidcircular": + UIGlowSolidCircular = new UISprite(subElement); + break; + case "uithermalglow": + UIThermalGlow = new UISprite(subElement); + break; case "endroundbuttonpulse": ButtonPulse = new UISprite(subElement); break; + case "iconoverflowindicator": + IconOverflowIndicator = new UISprite(subElement); + break; case "focusindicator": FocusIndicator = new SpriteSheet(subElement); break; @@ -277,6 +304,10 @@ namespace Barotrauma DigitalFont = LoadFont(subElement, graphicsDevice); ForceFontUpperCase[DigitalFont] = subElement.GetAttributeBool("forceuppercase", false); break; + case "monospacedfont": + MonospacedFont = LoadFont(subElement, graphicsDevice); + ForceFontUpperCase[MonospacedFont] = subElement.GetAttributeBool("forceuppercase", false); + break; case "hotkeyfont": HotkeyFont = LoadFont(subElement, graphicsDevice); ForceFontUpperCase[HotkeyFont] = subElement.GetAttributeBool("forceuppercase", false); @@ -446,7 +477,7 @@ namespace Barotrauma public void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) { - GUIComponentStyle componentStyle = null; + GUIComponentStyle componentStyle = null; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; @@ -461,7 +492,7 @@ namespace Barotrauma return; } } - + string childStyleName = string.IsNullOrEmpty(styleName) ? targetComponent.GetType().Name : styleName; parentStyle.ChildStyles.TryGetValue(childStyleName.ToLowerInvariant(), out componentStyle); } @@ -477,8 +508,25 @@ namespace Barotrauma return; } } - - targetComponent.ApplyStyle(componentStyle); + + targetComponent.ApplyStyle(componentStyle); + } + + public Color GetQualityColor(int quality) + { + switch (quality) + { + case 1: + return ItemQualityColorGood; + case 2: + return ItemQualityColorExcellent; + case 3: + return ItemQualityColorMasterwork; + case -1: + return ItemQualityColorPoor; + default: + return ItemQualityColorNormal; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 407236e08..69f18c269 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -185,7 +185,7 @@ namespace Barotrauma if (GUI.MouseOn != null) { return false; } //don't close when hovering over an inventory element - if (Inventory.IsMouseOnInventory()) { return false; } + if (Inventory.IsMouseOnInventory) { return false; } bool input = PlayerInput.PrimaryMouseButtonDown() || PlayerInput.SecondaryMouseButtonClicked(); return input && !rect.Contains(PlayerInput.MousePosition); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 23b0f9036..3e7e06550 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -81,7 +81,7 @@ namespace Barotrauma private readonly object loadMutex = new object(); private float? loadState; - + public float? LoadState { get @@ -90,8 +90,8 @@ namespace Barotrauma { return loadState; } - } - set + } + set { lock (loadMutex) { @@ -141,7 +141,7 @@ namespace Barotrauma GameMain.Config.EnableSplashScreen = false; } } - + var titleStyle = GUI.Style?.GetComponentStyle("TitleText"); Sprite titleSprite = null; if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) @@ -177,8 +177,8 @@ namespace Barotrauma color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); - titleSprite?.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth * 0.05f, GameMain.GraphicsHeight * 0.125f), - Color.White, origin: new Vector2(0.0f, titleSprite.SourceRect.Height / 2.0f), + titleSprite?.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth * 0.05f, GameMain.GraphicsHeight * 0.125f), + Color.White, origin: new Vector2(0.0f, titleSprite.SourceRect.Height / 2.0f), scale: GameMain.GraphicsHeight / 2000.0f); if (WaitForLanguageSelection) @@ -193,7 +193,7 @@ namespace Barotrauma if (LoadState == 100.0f) { #if DEBUG - if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled && GameMain.FirstLoad) + if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled || GameMain.Config.TestScreenEnabled && GameMain.FirstLoad) { loadText = "QUICKSTARTING ..."; } @@ -211,6 +211,13 @@ namespace Barotrauma if (LoadState != null) { loadText += " " + (int)LoadState + " %"; + +#if DEBUG + if (GameMain.FirstLoad && GameMain.CancelQuickStart) + { + loadText += " (Quickstart aborted)"; + } +#endif } } if (GUI.LargeFont != null) @@ -265,7 +272,7 @@ namespace Barotrauma decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), new Vector2(GameMain.GraphicsWidth * 0.001f, GameMain.GraphicsHeight * 0.24f), Color.White, Vector2.Zero, 0.0f, decorativeScale, SpriteEffects.FlipVertically); - + decorativeMap.Draw(spriteBatch, (int)(decorativeMap.FrameCount * noiseVal), new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.66f), Color.White, decorativeMap.FrameSize.ToVector2(), 0.0f, decorativeScale); @@ -281,9 +288,9 @@ namespace Barotrauma } else if (noiseVal < 0.5f) { - randText = - Rand.Int(100).ToString().PadLeft(2, '0') + " " + - Rand.Int(100).ToString().PadLeft(2, '0') + " " + + randText = + Rand.Int(100).ToString().PadLeft(2, '0') + " " + + Rand.Int(100).ToString().PadLeft(2, '0') + " " + Rand.Int(100).ToString().PadLeft(2, '0') + " " + Rand.Int(100).ToString().PadLeft(2, '0'); } @@ -299,12 +306,12 @@ namespace Barotrauma { if (languageSelectionFont == null) { - languageSelectionFont = new ScalableFont("Content/Fonts/NotoSans/NotoSans-Bold.ttf", + languageSelectionFont = new ScalableFont("Content/Fonts/NotoSans/NotoSans-Bold.ttf", (uint)(30 * (GameMain.GraphicsHeight / 1080.0f)), graphicsDevice); } if (languageSelectionFontCJK == null) { - languageSelectionFontCJK = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", + languageSelectionFontCJK = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", (uint)(30 * (GameMain.GraphicsHeight / 1080.0f)), graphicsDevice, dynamicLoading: true); } if (languageSelectionCursor == null) @@ -320,11 +327,11 @@ namespace Barotrauma var font = TextManager.IsCJK(localizedLanguageName) ? languageSelectionFontCJK : languageSelectionFont; Vector2 textSize = font.MeasureString(localizedLanguageName); - bool hover = - Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 && + bool hover = + Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 && Math.Abs(PlayerInput.MousePosition.Y - textPos.Y) < textSpacing.Y / 2; - font.DrawString(spriteBatch, localizedLanguageName, textPos - textSize / 2, + font.DrawString(spriteBatch, localizedLanguageName, textPos - textSize / 2, hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { @@ -394,14 +401,14 @@ namespace Barotrauma LoadState = null; SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); currentBackgroundTexture = LocationType.List.GetRandom()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; - + while (!drawn) { yield return CoroutineStatus.Running; } CoroutineManager.StartCoroutine(loader); - + yield return CoroutineStatus.Running; while (CoroutineManager.IsCoroutineRunning(loader.ToString())) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs index 8bf8adea1..afb00b206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ShapeExtensions.cs @@ -55,14 +55,47 @@ namespace Barotrauma var texture = GetTexture(spriteBatch); for (var i = 0; i < points.Count - 1; i++) - DrawPolygonEdge(spriteBatch, texture, points[i] + offset, points[i + 1] + offset, color, thickness); + DrawPolygonEdge(spriteBatch, points[i] + offset, points[i + 1] + offset, color, thickness); - DrawPolygonEdge(spriteBatch, texture, points[points.Count - 1] + offset, points[0] + offset, color, + DrawPolygonEdge(spriteBatch, points[points.Count - 1] + offset, points[0] + offset, color, thickness); } + + /// + /// Draws a closed polygon from an array of points + /// + public static void DrawPolygonInner(this SpriteBatch spriteBatch, Vector2 offset, IReadOnlyList points, Color color, float thickness = 1f) + { + if (points.Count == 0) { return; } - private static void DrawPolygonEdge(SpriteBatch spriteBatch, Texture2D texture, Vector2 point1, Vector2 point2, - Color color, float thickness) + if (points.Count == 1) + { + DrawPoint(spriteBatch, points[0], color, (int)thickness); + return; + } + + for (var i = 0; i < points.Count - 1; i++) + { + Vector2 point1 = points[i] + offset, + point2 = points[i + 1] + offset; + + DrawPolygonEdgeInner(spriteBatch, point1, point2, color, thickness); + } + + DrawPolygonEdgeInner(spriteBatch, points[^1] + offset, points[0] + offset, color, thickness); + } + + private static void DrawPolygonEdgeInner(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) + { + var length = Vector2.Distance(point1, point2) + thickness; + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + var scale = new Vector2(length, thickness); + Vector2 middle = new Vector2((point1.X + point2.X) / 2f, (point1.Y + point2.Y) / 2f); + Texture2D tex = GetTexture(spriteBatch); + spriteBatch.Draw(GetTexture(spriteBatch), middle, null, color, angle, new Vector2(tex.Width / 2f, tex.Height / 2f), scale, SpriteEffects.None, 0); + } + + private static void DrawPolygonEdge(SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness) { var length = Vector2.Distance(point1, point2); var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 5c97c009d..114c2282e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -37,7 +37,7 @@ namespace Barotrauma private Color storeSpecialColor; private GUIListBox shoppingCrateBuyList, shoppingCrateSellList, shoppingCrateSellFromSubList; - private GUITextBlock shoppingCrateTotal; + private GUITextBlock relevantBalanceName, shoppingCrateTotal; private GUIButton clearAllButton, confirmButton; private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh; @@ -58,7 +58,6 @@ namespace Barotrauma StoreTab.SellFromSub => false, _ => throw new NotImplementedException() }; - private bool IsSelling => !IsBuying; private GUIListBox ActiveShoppingCrateList => activeTab switch { StoreTab.Buy => shoppingCrateBuyList, @@ -222,16 +221,8 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) - { - merchantBalanceBlock.TextColor = CurrentLocation.BalanceColor; - return GetCurrencyFormatted(CurrentLocation.StoreCurrentBalance); - } - else - { - merchantBalanceBlock.TextColor = Color.Red; - return GetCurrencyFormatted(0); - } + merchantBalanceBlock.TextColor = CurrentLocation?.BalanceColor ?? Color.Red; + return GetMerchantBalanceText(); } }; @@ -375,28 +366,37 @@ namespace Barotrauma //don't show categories with no buyable items itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought)); itemCategoryButtons.Clear(); + var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), style: "CategoryButton.All") + { + ToolTip = TextManager.Get("MapEntityCategory.All"), + OnClicked = OnClickedCategoryButton + }; + itemCategoryButtons.Add(categoryButton); foreach (MapEntityCategory category in itemCategories) { - var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), + categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), style: "CategoryButton." + category) { ToolTip = TextManager.Get("MapEntityCategory." + category), UserData = category, - OnClicked = (btn, userdata) => - { - MapEntityCategory? newCategory = !btn.Selected ? (MapEntityCategory?)userdata : null; - if (newCategory.HasValue) { searchBox.Text = ""; } - if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; } - FilterStoreItems(newCategory, searchBox.Text); - return true; - } + OnClicked = OnClickedCategoryButton }; itemCategoryButtons.Add(categoryButton); - categoryButton.RectTransform.SizeChanged += () => + } + bool OnClickedCategoryButton(GUIButton button, object userData) + { + MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null; + if (newCategory.HasValue) { searchBox.Text = ""; } + if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; } + FilterStoreItems(newCategory, searchBox.Text); + return true; + } + foreach (var btn in itemCategoryButtons) + { + btn.RectTransform.SizeChanged += () => { - var sprite = categoryButton.Frame.sprites[GUIComponent.ComponentState.None].First(); - categoryButton.RectTransform.NonScaledSize = - new Point(categoryButton.Rect.Width, (int)(categoryButton.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); + var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First(); + btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width))); }; } @@ -503,11 +503,11 @@ namespace Barotrauma ForceUpperCase = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => GetCurrencyFormatted(PlayerMoney) + TextGetter = GetPlayerBalanceText }; // Divider ------------------------------------------------ @@ -523,7 +523,7 @@ namespace Barotrauma RelativeSpacing = 0.015f, Stretch = true }; - var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), shoppingCrateInventoryContainer.RectTransform), style: null); + var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; if (GameMain.IsSingleplayer) @@ -531,6 +531,21 @@ namespace Barotrauma shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; } + var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUI.Font) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + { + CanBeFocused = false, + TextScale = 1.1f, + TextGetter = () => IsBuying ? GetPlayerBalanceText() : GetMerchantBalanceText() + }; + var totalContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { Stretch = true @@ -576,10 +591,14 @@ namespace Barotrauma resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private GUILayoutGroup CreateDealsGroup(GUIListBox parentList) + private string GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + + private string GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + + private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { var elementHeight = (int)(GUI.yScale * 80); - var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, 4 * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); + var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); frame.UserData = "deals"; var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter); var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -604,17 +623,14 @@ namespace Barotrauma { prevLocation.Reputation.OnReputationValueChanged = null; } - - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _))) { - if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _)) + selectedItemCategory = null; + searchBox.Text = ""; + ChangeStoreTab(StoreTab.Buy); + if (newLocation?.Reputation != null) { - ChangeStoreTab(StoreTab.Buy); - if (newLocation?.Reputation != null) - { - newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; - } - return; + newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; } } } @@ -628,6 +644,7 @@ namespace Barotrauma tabButton.Selected = (StoreTab)tabButton.UserData == activeTab; } sortingDropDown.SelectItem(tabSortingMethods[tab]); + relevantBalanceName.Text = IsBuying ? TextManager.Get("campaignstore.balance") : TextManager.Get("campaignstore.storebalance"); SetShoppingCrateTotalText(); SetClearAllButtonStatus(); SetConfirmButtonBehavior(); @@ -697,7 +714,7 @@ namespace Barotrauma } foreach (GUIButton btn in itemCategoryButtons) { - btn.Selected = category.HasValue && (MapEntityCategory)btn.UserData == selectedItemCategory; + btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory; } list.UpdateScrollBarSize(); } @@ -709,6 +726,8 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } + int prevDailySpecialCount; + private void RefreshStoreBuyList() { float prevBuyListScroll = storeBuyList.BarScroll; @@ -717,11 +736,14 @@ namespace Barotrauma bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); - if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any()) + int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; + + if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { - if (storeDailySpecialsGroup == null) + if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) { - storeDailySpecialsGroup = CreateDealsGroup(storeBuyList); + storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, 1 + dailySpecialCount); storeDailySpecialsGroup.Parent.SetAsFirstChild(); } else @@ -730,6 +752,7 @@ namespace Barotrauma storeDailySpecialsGroup = null; } storeBuyList.RecalculateChildren(); + prevDailySpecialCount = dailySpecialCount; } foreach (PurchasedItem item in CurrentLocation.StoreStock) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index a5239b1b6..6ed653ddc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Linq; @@ -18,8 +19,8 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - private enum InfoFrameTab { Crew, Mission, Reputation, MyCharacter, Traitor, Submarine }; - private static InfoFrameTab selectedTab; + public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; + public static InfoFrameTab selectedTab; private GUIFrame infoFrame, contentFrame; private readonly List tabButtons = new List(); @@ -33,6 +34,8 @@ namespace Barotrauma private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; + private GUIFrame pendingChangesFrame = null; + public static Color OwnCharacterBGColor = Color.Gold * 0.7f; private class LinkedGUI @@ -48,7 +51,7 @@ namespace Barotrauma private readonly GUIFrame frame; public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) - { + { this.client = client; this.textBlock = textBlock; this.frame = frame; @@ -125,7 +128,7 @@ namespace Barotrauma public TabMenu() { - if (!initialized) Initialize(); + if (!initialized) { Initialize(); } CreateInfoFrame(selectedTab); SelectInfoFrameTab(null, selectedTab); @@ -133,6 +136,17 @@ namespace Barotrauma public void Update() { + GameSession.UpdateTalentNotificationIndicator(talentPointNotification); + if (Character.Controlled is { } controlled && talentResetButton != null && talentApplyButton != null) + { + int talentCount = selectedTalents.Count - controlled.Info.GetUnlockedTalentsInTree().Count(); + talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) + { + talentApplyButton.Flash(GUI.Style.Orange); + } + } + if (selectedTab != InfoFrameTab.Crew) return; if (linkedGUIList == null) return; @@ -182,16 +196,8 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); //this used to be a switch expression but i changed it because it killed enc :( - Vector2 contentFrameSize; - switch (selectedTab) - { - case InfoFrameTab.MyCharacter: - contentFrameSize = new Vector2(0.45f, 0.5f); - break; - default: - contentFrameSize = new Vector2(0.45f, 0.667f); - break; - } + //now it's not even a switch statement anymore :( + Vector2 contentFrameSize = new Vector2(0.45f, 0.667f); contentFrame = new GUIFrame(new RectTransform(contentFrameSize, infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.12f) }); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(25f)) }, isHorizontal: true) @@ -242,6 +248,17 @@ namespace Barotrauma { TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) }; + GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform) + { + AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) + }, style: null); + + pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); + + if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false) + { + NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + } } else { @@ -254,10 +271,17 @@ namespace Barotrauma var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); - if (GameMain.NetworkMember != null) + var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); + talentsButton.OnAddedToGUIUpdateList += (component) => { - var myCharacterButton = createTabButton(InfoFrameTab.MyCharacter, "tabmenu.character"); - } + talentsButton.Enabled = Character.Controlled?.Info != null; + if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) + { + SelectInfoFrameTab(null, InfoFrameTab.Crew); + } + }; + + talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); } private bool SelectInfoFrameTab(GUIButton button, object userData) @@ -289,13 +313,12 @@ namespace Barotrauma if (traitor == null || traitorMission == null) return false; CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); break; - case InfoFrameTab.MyCharacter: - if (GameMain.NetworkMember == null) { return false; } - GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameHolder); - break; case InfoFrameTab.Submarine: CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; + case InfoFrameTab.Talents: + CreateTalentInfo(infoFrameHolder); + break; } return true; @@ -408,7 +431,7 @@ namespace Barotrauma if (GameMain.IsMultiplayer) { CreateMultiPlayerList(false); - CreateMultiPlayerLogContent(crewFrame); + CreateMultiPlayerLogContent(crewFrame); } else { @@ -599,7 +622,7 @@ namespace Barotrauma AbsoluteSpacing = 2 }; - new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)) { CanBeFocused = false, @@ -828,7 +851,7 @@ namespace Barotrauma } private static readonly List> storedMessages = new List>(); - + public static void StorePlayerConnectionChangeMessage(ChatMessage message) { if (!GameMain.GameSession?.IsRunning ?? true) { return; } @@ -841,7 +864,7 @@ namespace Barotrauma TabMenu instance = GameSession.TabMenuInstance; instance.AddLineToLog(msg, message.ChangeType); instance.RemoveCurrentElements(); - instance.CreateMultiPlayerList(true); + instance.CreateMultiPlayerList(true); } } @@ -960,7 +983,7 @@ namespace Barotrauma GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) { - AbsoluteSpacing = spacing + AbsoluteSpacing = spacing }; string descriptionText = mission.Description; foreach (string missionMessage in mission.ShownMessages) @@ -988,7 +1011,7 @@ namespace Barotrauma float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; bool displayDifficulty = mission.Difficulty.HasValue; if (displayDifficulty) { ySize += missionRewardSize.Y; } - + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)ySize); missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); @@ -999,8 +1022,8 @@ namespace Barotrauma int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); Point iconSize = new Point(iconWidth, iconHeight);*/ - new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) - { + new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + { Color = mission.Prefab.IconColor, HoverColor = mission.Prefab.IconColor, SelectedColor = mission.Prefab.IconColor, @@ -1159,5 +1182,506 @@ namespace Barotrauma sub.Info.CreateSpecsWindow(specsListBox, GUI.Font, includeTitle: false, includeClass: false, includeDescription: true); } } + private Color unselectedColor = new Color(240, 255, 255, 225); + private Color selectedColor = new Color(220, 255, 220, 225); + private Color ownedColor = new Color(140, 180, 140, 225); + private Color unselectableColor = new Color(100, 100, 100, 225); + private Color pressedColor = new Color(60, 60, 60, 225); + + private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>(); + private readonly List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); + private List selectedTalents = new List(); + + private GUITextBlock experienceText; + private GUIProgressBar experienceBar; + private GUITextBlock talentPointText; + private GUIListBox skillListBox; + + private GUIButton talentApplyButton, + talentResetButton; + + private GUIImage talentPointNotification; + + private readonly ImmutableDictionary talentStageStyles = new Dictionary + { + { TalentTree.TalentTreeStageState.Invalid, GUI.Style.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Locked, GUI.Style.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Unlocked, GUI.Style.GetComponentStyle("TalentTreePurchased") }, + { TalentTree.TalentTreeStageState.Available, GUI.Style.GetComponentStyle("TalentTreeUnlocked") }, + { TalentTree.TalentTreeStageState.Highlighted, GUI.Style.GetComponentStyle("TalentTreeAvailable") }, + }.ToImmutableDictionary(); + + private readonly ImmutableDictionary talentStageBackgroundColors = new Dictionary + { + { TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) }, + { TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) }, + { TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) }, + { TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) }, + { TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) }, + }.ToImmutableDictionary(); + + private void CreateTalentInfo(GUIFrame infoFrame) + { + infoFrame.ClearChildren(); + talentButtons.Clear(); + talentCornerIcons.Clear(); + + Character controlledCharacter = Character.Controlled; + if (controlledCharacter == null) { return; } + + GUIFrame talentFrameBackground = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = GUI.IntScale(15); + GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); + + GUIFrame paddedTalentFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), talentFrameContent.RectTransform, Anchor.Center), style: null); + + GUIFrame talentFrameMain = new GUIFrame(new RectTransform(Vector2.One, paddedTalentFrame.RectTransform), style: null); + + GUIFrame characterSettingsFrame = null; + GUILayoutGroup characterLayout = null; + if (!(GameMain.NetworkMember is null)) + { + characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrameContent.RectTransform), style: null) { Visible = false }; + characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); + GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); + GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); + GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); + } + + if (controlledCharacter.Info is null) + { + DebugConsole.ThrowError("No character info found for talent UI"); + return; + } + + selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); + + GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameMain.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); + + CharacterInfo info = controlledCharacter.Info; + Job job = info.Job; + + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), talentInfoLayoutGroup.RectTransform), onDraw: (batch, component) => + { + float posY = component.Rect.Center.Y - component.Rect.Width / 2; + info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); + }); + + GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; + + Vector2 nameSize = GUI.SubHeadingFont.MeasureString(info.Name); + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUI.SubHeadingFont) { TextColor = job.Prefab.UIColor }; + nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); + + Vector2 jobSize = GUI.SmallFont.MeasureString(job.Name); + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUI.SmallFont) { TextColor = job.Prefab.UIColor }; + jobBlock.RectTransform.NonScaledSize = jobSize.Pad(jobBlock.Padding).ToPoint(); + + string traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); + Vector2 traitSize = GUI.SmallFont.MeasureString(traitString); + GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUI.SmallFont); + traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); + + GUIFrame endocrineFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), nameLayout.RectTransform, Anchor.BottomCenter), style: null); + + if (!(GameMain.NetworkMember is null)) + { + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.675f, 1f), endocrineFrame.RectTransform, Anchor.TopLeft), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + { + IgnoreLayoutGroups = true + }; + + newCharacterBox.OnClicked = (button, o) => + { + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + + if (pendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + } + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + talentFrameMain.Visible = false; + } + }; + + if (!(characterLayout is null)) + { + GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomRight); + new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? + { + OnClicked = (button, o) => + { + characterSettingsFrame!.Visible = false; + talentFrameMain.Visible = true; + return true; + } + }; + } + } + + IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(e, StringComparison.OrdinalIgnoreCase))); + + if (endocrineTalents.Count() > 0) + { + GUIImage endocrineIcon = new GUIImage(new RectTransform(new Vector2(0.275f, 1f), endocrineFrame.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.Normal), style: "EndocrineReminderIcon") + { + ToolTip = $"{TextManager.Get("afflictionname.endocrineboost")}\n\n{string.Join(", ", endocrineTalents.Select(e => e.DisplayName))}" + }; + } + + GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; + + string skillString = TextManager.Get("skills"); + Vector2 skillSize = GUI.SubHeadingFont.MeasureString(skillString); + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUI.SubHeadingFont); + skillBlock.RectTransform.NonScaledSize = skillSize.Pad(skillBlock.Padding).ToPoint(); + + skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); + CreateTalentSkillList(controlledCharacter, skillListBox); + + if (!TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + + new GUIFrame(new RectTransform(new Vector2(1f, 1f), talentFrameLayoutGroup.RectTransform), style: "HorizontalLine"); + + GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + + List subTreeNames = new List(); + foreach (var subTree in talentTree.TalentSubTrees) + { + GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); + + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + int elementPadding = GUI.IntScale(8); + Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; + GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUI.SubHeadingFont, textAlignment: Alignment.Center)); + + for (int i = 0; i < 4; i++) + { + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); + + Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + + GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground"); + GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; + + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + { + CanBeFocused = false + }; + + Point iconSize = cornerIcon.RectTransform.NonScaledSize; + cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + + if (subTree.TalentOptionStages.Count > i) + { + TalentOption talentOption = subTree.TalentOptionStages[i]; + + GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); + + GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + + foreach (TalentPrefab talent in talentOption.Talents) + { + GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) + { + CanBeFocused = false + }; + + GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); + + GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) + { + ToolTip = $"{talent.DisplayName}\n\n{talent.Description}", + UserData = talent.Identifier, + PressedColor = pressedColor, + OnClicked = (button, userData) => + { + // deselect other buttons in tier by removing their selected talents from pool + foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) + { + if (guiButton.UserData is string otherTalentIdentifier && guiButton != button) + { + if (!controlledCharacter.HasTalent(otherTalentIdentifier)) + { + selectedTalents.Remove(otherTalentIdentifier); + } + } + } + string talentIdentifier = userData as string; + + if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) + { + if (!selectedTalents.Contains(talentIdentifier)) + { + selectedTalents.Add(talentIdentifier); + } + } + else if (!controlledCharacter.HasTalent(talentIdentifier)) + { + selectedTalents.Remove(talentIdentifier); + } + + UpdateTalentButtons(); + return true; + }, + }; + + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = Color.Transparent; + + GUIComponent iconImage; + if (talent.Icon is null) + { + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) + { + OutlineColor = GUI.Style.Red, + TextColor = GUI.Style.Red, + PressedColor = unselectableColor, + CanBeFocused = false, + }; + } + else + { + iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) + { + PressedColor = unselectableColor, + CanBeFocused = false, + }; + } + + talentButtons.Add((talentButton, iconImage)); + } + + talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); + } + } + } + GUITextBlock.AutoScaleAndNormalize(subTreeNames); + + GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; + + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); + + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUI.Style.Green) + { + IsHorizontal = true + }; + + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight) + { + Shadow = true + }; + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUI.SubHeadingFont, parseRichText: true, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; + + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + { + OnClicked = ApplyTalentSelection, + }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + + UpdateTalentButtons(); + } + + private void CreateTalentSkillList(Character character, GUIListBox parent) + { + parent.Content.ClearChildren(); + List skillNames = new List(); + foreach (Skill skill in character.Info.Job.Skills) + { + GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; + + skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}", returnNull: true) ?? skill.Identifier)); + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; + + float modifiedSkillLevel = character.GetSkillLevel(skill.Identifier); + if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) + { + int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + string stringColor = true switch + { + true when skillChange > 0 => XMLExtensions.ColorToString(GUI.Style.Green), + true when skillChange < 0 => XMLExtensions.ColorToString(GUI.Style.Red), + _ => XMLExtensions.ColorToString(GUI.Style.TextColor) + }; + + string changeText = $"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"; + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText, parseRichText: true) { Padding = Vector4.Zero }; + } + skillContainer.Recalculate(); + } + + parent.RecalculateChildren(); + GUITextBlock.AutoScaleAndNormalize(skillNames); + } + + private void UpdateTalentButtons() + { + Character controlledCharacter = Character.Controlled; + + experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); + //experienceBar.ToolTip = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; + + selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); + + string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString(); + + int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count(); + + if (talentCount > 0) + { + string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUI.Style.Red)}‖{-talentCount}‖color:end‖"; + string localizedString = TextManager.GetWithVariables("talentmenu.points.spending", new []{ "[amount]", "[used]" }, new []{ pointsLeft, pointsUsed}); + talentPointText.SetRichText(localizedString); + } + else + { + talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft)); + } + + foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons) + { + TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents); + GUIComponentStyle newStyle = talentStageStyles[state]; + icon.ApplyStyle(newStyle); + icon.Color = newStyle.Color; + frame.Color = talentStageBackgroundColors[state]; + glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted; + } + + foreach (var talentButton in talentButtons) + { + string talentIdentifier = talentButton.button.UserData as string; + bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); + Color newTalentColor = unselectable ? unselectableColor : unselectedColor; + Color hoverColor = Color.White; + + if (controlledCharacter.HasTalent(talentIdentifier)) + { + newTalentColor = GUI.Style.Green; + } + else if (selectedTalents.Contains(talentIdentifier)) + { + newTalentColor = GUI.Style.Orange; + hoverColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.7f); + } + + talentButton.icon.Color = newTalentColor; + talentButton.icon.HoverColor = hoverColor; + } + + CreateTalentSkillList(controlledCharacter, skillListBox); + } + + private void ApplyTalents(Character controlledCharacter) + { + selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); + foreach (string talent in selectedTalents) + { + controlledCharacter.GiveTalent(talent); + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(controlledCharacter, new object[] { NetEntityEvent.Type.UpdateTalents }); + } + } + UpdateTalentButtons(); + } + + private bool ApplyTalentSelection(GUIButton guiButton, object userData) + { + Character controlledCharacter = Character.Controlled; + ApplyTalents(controlledCharacter); + return true; + } + + private bool ResetTalentSelection(GUIButton guiButton, object userData) + { + Character controlledCharacter = Character.Controlled; + selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); + UpdateTalentButtons(); + return true; + } + + public void OnExperienceChanged(Character character) + { + if (character != Character.Controlled) { return; } + UpdateTalentButtons(); + } + + private readonly StatTypes[] basicStats = new StatTypes[] + { + StatTypes.MaximumHealthMultiplier, + StatTypes.MovementSpeed, + StatTypes.SwimmingSpeed, + StatTypes.RepairSpeed, + }; + + private readonly StatTypes[] combatStats = new StatTypes[] + { + StatTypes.MeleeAttackMultiplier, + StatTypes.MeleeAttackSpeed, + StatTypes.RangedAttackSpeed, + StatTypes.TurretAttackSpeed, + }; + + private readonly StatTypes[] miscStats = new StatTypes[] + { + StatTypes.ReputationGainMultiplier, + StatTypes.MissionMoneyGainMultiplier, + StatTypes.ExperienceGainMultiplier, + StatTypes.MissionExperienceGainMultiplier, + }; + + private void CreateCharacterSheet(GUILayoutGroup characterInfoColumn) + { + Character controlledCharacter = Character.Controlled; + + CreateRow(basicStats); + CreateRow(combatStats); + CreateRow(miscStats); + + void CreateRow(StatTypes[] statTypes) + { + GUILayoutGroup characterInfoRow = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1.0f), characterInfoColumn.RectTransform, anchor: Anchor.TopLeft), childAnchor: Anchor.TopCenter); + foreach (StatTypes statType in statTypes) + { + ShowStat(statType, characterInfoRow); + } + } + + void ShowStat(StatTypes statType, GUILayoutGroup characterInfoRow) + { + GUIFrame textInfoFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), characterInfoRow.RectTransform, Anchor.TopCenter), style: null); + new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), statType.ToString(), font: GUI.SmallFont, textAlignment: Alignment.TopLeft); + new GUITextBlock(new RectTransform(new Vector2(1f, 1f), textInfoFrame.RectTransform, Anchor.TopLeft), (int)(100f * (1 + controlledCharacter.GetStatValue(statType))) + "%", font: GUI.Font, textAlignment: Alignment.TopRight); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index a6b3ca8dc..4d79d57fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -115,8 +115,9 @@ namespace Barotrauma return MathHelper.Clamp(Math.Min(Math.Min(scale.X, scale.Y), GUI.SlicedSpriteScale), minBorderScale, maxBorderScale); } - public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None, Vector2? uvOffset = null) { + uvOffset ??= Vector2.Zero; if (Sprite.Texture == null) { GUI.DrawRectangle(spriteBatch, rect, Color.Magenta); @@ -157,7 +158,7 @@ namespace Barotrauma else if (Tile) { Vector2 startPos = new Vector2(rect.X, rect.Y); - Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color); + Sprite.DrawTiled(spriteBatch, startPos, new Vector2(rect.Width, rect.Height), color, startOffset: uvOffset); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 083551ad0..9f9234a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1045,10 +1045,10 @@ namespace Barotrauma public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); - return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton); + return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } - public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, string title, string body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton") + public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, string title, string body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab upgradePrefab = null, int currentLevel = 0) { float progressBarHeight = 0.25f; @@ -1089,7 +1089,7 @@ namespace Barotrauma //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - var priceText = new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); + var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); if (price < 0) { priceText.TextColor = GUI.Style.Green; @@ -1099,6 +1099,11 @@ namespace Barotrauma priceText.Text = string.Empty; } new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) { Enabled = false }; + if (upgradePrefab != null) + { + var increaseText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), "", textAlignment: Alignment.Center); + UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel); + } } description.CalculateHeightFromText(); @@ -1127,6 +1132,19 @@ namespace Barotrauma return prefabFrame; } + private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel) + { + float nextIncrease = upgradePrefab.IncreaseOnTooltip * (Math.Min(currentLevel + 1, upgradePrefab.MaxLevel)); + if (nextIncrease != 0f) + { + text.Text = $"{Math.Round(nextIncrease, 1)} %"; + if (currentLevel == upgradePrefab.MaxLevel) + { + text.TextColor = Color.Gray; + } + } + } + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List? itemsOnSubmarine) { if (Campaign is null) { return; } @@ -1541,7 +1559,9 @@ namespace Barotrauma if (prefabFrame.FindChild("buybutton", true) is { } buttonParent) { - GUITextBlock priceLabel = buttonParent.GetChild(); + List textBlocks = buttonParent.GetAllChildren().ToList(); + + GUITextBlock priceLabel = textBlocks[0]; int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); if (priceLabel != null && !WaitForServerUpdate) @@ -1562,6 +1582,11 @@ namespace Barotrauma button.Enabled = false; } } + GUITextBlock increaseLabel = textBlocks[1]; + if (increaseLabel != null && !WaitForServerUpdate) + { + UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index b96abab6a..60cf0e8a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -45,6 +45,7 @@ namespace Barotrauma public static SteamWorkshopScreen SteamWorkshopScreen; public static SubEditorScreen SubEditorScreen; + public static TestScreen TestScreen; public static ParticleEditorScreen ParticleEditorScreen; public static LevelEditorScreen LevelEditorScreen; public static SpriteEditorScreen SpriteEditorScreen; @@ -91,7 +92,16 @@ namespace Barotrauma public static ParticleManager ParticleManager; public static DecalManager DecalManager; - public static World World; + private static World world; + public static World World + { + get + { + if (world == null) { world = new World(new Vector2(0, -9.82f)); } + return world; + } + set { world = value; } + } public static LoadingScreen TitleScreen; private bool loadingScreenOpen; @@ -186,6 +196,8 @@ namespace Barotrauma #if DEBUG public static bool FirstLoad = true; + + public static bool CancelQuickStart; #endif public GameMain(string[] args) @@ -243,7 +255,6 @@ namespace Barotrauma GameMain.ResetFrameTime(); fixedTime = new GameTime(); - World = new World(new Vector2(0, -9.82f)); FarseerPhysics.Settings.AllowSleep = true; FarseerPhysics.Settings.ContinuousPhysics = false; FarseerPhysics.Settings.VelocityIterations = 1; @@ -304,7 +315,7 @@ namespace Barotrauma GraphicsDeviceManager.PreferredBackBufferWidth = GraphicsWidth; GraphicsDeviceManager.PreferredBackBufferHeight = GraphicsHeight; - + GraphicsDeviceManager.ApplyChanges(); if (windowMode == WindowMode.BorderlessWindowed) @@ -571,6 +582,8 @@ namespace Barotrauma ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); + TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); + TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); Order.Init(); EventManagerSettings.Init(); BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); @@ -581,7 +594,7 @@ namespace Barotrauma StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); TitleScreen.LoadState = 55.0f; yield return CoroutineStatus.Running; - + UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); TitleScreen.LoadState = 56.0f; yield return CoroutineStatus.Running; @@ -594,7 +607,7 @@ namespace Barotrauma ItemAssemblyPrefab.LoadAll(); TitleScreen.LoadState = 60.0f; yield return CoroutineStatus.Running; - + GameModePreset.Init(); SaveUtil.DeleteDownloadedSubs(); @@ -624,6 +637,7 @@ namespace Barotrauma #endif SubEditorScreen = new SubEditorScreen(); + TestScreen = new TestScreen(); TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; @@ -646,7 +660,7 @@ namespace Barotrauma ParticleManager.LoadPrefabs(); TitleScreen.LoadState = 88.0f; LevelObjectPrefab.LoadAll(); - + TitleScreen.LoadState = 90.0f; yield return CoroutineStatus.Running; @@ -796,12 +810,18 @@ namespace Barotrauma } #if DEBUG - if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled) && FirstLoad && !PlayerInput.KeyDown(Keys.LeftShift)) + CancelQuickStart |= PlayerInput.KeyDown(Keys.LeftShift); + + if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !CancelQuickStart) { loadingScreenOpen = false; FirstLoad = false; - if (Config.AutomaticQuickStartEnabled) + if (Config.TestScreenEnabled) + { + TestScreen.Select(); + } + else if (Config.AutomaticQuickStartEnabled) { MainMenuScreen.QuickStart(); } @@ -906,8 +926,7 @@ namespace Barotrauma } //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC else if ((Character.Controlled == null || !itemHudActive()) - //TODO: do we need to check Inventory.SelectedSlot? - && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null + && CharacterHealth.OpenHealthWindow == null && !CrewManager.IsCommandInterfaceOpen && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedConstruction != null)) { @@ -918,8 +937,8 @@ namespace Barotrauma static bool itemHudActive() { if (Character.Controlled?.SelectedConstruction == null) { return false; } - return - Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null) || + return + Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null) || ((Character.Controlled.ViewTarget as Item)?.Prefab?.FocusOnSelected ?? false); } } @@ -1054,7 +1073,6 @@ namespace Barotrauma spriteBatch.End(); } - sw.Stop(); PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); @@ -1085,7 +1103,7 @@ namespace Barotrauma if (save) { GUI.SetSavingIndicatorState(true); - + if (GameSession.Submarine != null && !GameSession.Submarine.Removed) { GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); @@ -1257,7 +1275,7 @@ namespace Barotrauma string text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); string extensionText = TextManager.Get(promptExtensionTag, returnNull: true, useEnglishAsFallBack: false); if (!string.IsNullOrEmpty(extensionText)) - { + { text += $"\n\n{extensionText}"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index cffadc3ee..3010c608a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -48,11 +48,7 @@ namespace Barotrauma var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; return character.Inventory.FindAllItems(item => { - if (item.SpawnedInOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - // There must be no contained items or the contained items must be confirmed as sold - if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } // Item must be in a non-equipment slot if possible if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } // Item must not be contained inside an item in an equipment slot @@ -76,15 +72,10 @@ namespace Barotrauma var confirmedSoldEntities = GetConfirmedSoldEntities(); return Submarine.MainSub.GetItems(true).FindAll(item => { - if (!item.Prefab.CanBeSold) { return false; } - if (item.SpawnedInOutpost) { return false; } - if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } if (!ItemAndAllContainersInteractable(item)) { return false; } - if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } - // There must be no contained items or the contained items must be confirmed as sold - if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } return true; }).Distinct(); @@ -107,6 +98,24 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } + private bool IsItemSellable(Item item, IEnumerable confirmedSoldEntities) + { + if (!item.Prefab.CanBeSold) { return false; } + if (item.SpawnedInOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } + if (item.OwnInventory?.Container is ItemContainer itemContainer) + { + var containedItems = item.ContainedItems; + if (containedItems.None()) { return true; } + // Allow selling the item if contained items are unsellable and set to be removed on deconstruct + if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; } + // Otherwise there must be no contained items or the contained items must be confirmed as sold + if (!containedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + } + return true; + } + public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5cc69eb70..09fe317e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -17,6 +17,13 @@ namespace Barotrauma { private Point screenResolution; + public Order DraggedOrder; + public bool DragOrder; + private bool dropOrder; + private int framesToSkip = 2; + private float dragOrderTreshold; + private Vector2 dragPoint = Vector2.Zero; + #region UI public GUIComponent ReportButtonFrame { get; set; } @@ -93,9 +100,12 @@ namespace Barotrauma { AutoHideScrollBar = false, CanBeFocused = false, + CanDragElements = true, + CanInteractWhenUnfocusable = true, OnSelected = (component, userData) => false, SelectMultiple = false, - Spacing = (int)(GUI.Scale * 10) + Spacing = (int)(GUI.Scale * 10), + OnRearranged = OnCrewListRearranged }; jobIndicatorBackground = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(0, 512, 128, 128)); @@ -180,7 +190,7 @@ namespace Barotrauma }; } - var reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -198,25 +208,54 @@ namespace Barotrauma ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(0, -chatBox.ToggleButton.Rect.Height); + CreateReportButtons(this, ReportButtonFrame, reports, false); + + #endregion + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + prevUIScale = GUI.Scale; + _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; + dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); + } + + public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, List reports, bool isHorizontal) + { //report buttons foreach (Order order in reports) { if (!order.IsReport || order.SymbolSprite == null || order.Hidden) { continue; } - var btn = new GUIButton(new RectTransform(new Point(ReportButtonFrame.Rect.Width), ReportButtonFrame.RectTransform), style: null) + var btn = new GUIButton(new RectTransform(new Point(isHorizontal ? parent.Rect.Height : parent.Rect.Width), parent.RectTransform), style: null) { - OnClicked = (GUIButton button, object userData) => + OnClicked = (button, userData) => { - if (!CanIssueOrders) { return false; } + if (!CanIssueOrders || crewManager?.DraggedOrder != null) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - if (IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } + + if (crewManager != null) + { + crewManager.SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } + } return true; }, UserData = order, - ToolTip = order.Name, ClampMouseRectToParent = false }; + btn.ToolTip = $"‖color:{XMLExtensions.ColorToString(order.Prefab.Color)}‖{order.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}"; + + if (crewManager != null) + { + btn.OnButtonDown = () => + { + crewManager.dragOrderTreshold = Math.Max(btn.Rect.Width, btn.Rect.Height) / 2f; + crewManager.DraggedOrder = order; + crewManager.dropOrder = false; + crewManager.framesToSkip = 2; + crewManager.dragPoint = btn.Rect.Center.ToVector2(); + return true; + }; + } new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") { @@ -232,17 +271,11 @@ namespace Barotrauma { Color = order.Color, HoverColor = Color.Lerp(order.Color, Color.White, 0.5f), - ToolTip = order.Name, - SpriteEffects = SpriteEffects.FlipHorizontally + ToolTip = btn.RawToolTip, + SpriteEffects = SpriteEffects.FlipHorizontally, + UserData = order }; } - - #endregion - - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - prevUIScale = GUI.Scale; - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); } #endregion @@ -291,8 +324,19 @@ namespace Barotrauma new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { - UserData = character + UserData = character, + OnSecondaryClicked = (comp, data) => + { + if (data == null) { return false; } + if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) + { + CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + } + return false; + } }; + SetCharacterComponentTooltip(background); var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; @@ -310,7 +354,10 @@ namespace Barotrauma var paddingRelativeWidth = 0.35f * commandButtonAbsoluteHeight / background.Rect.Width; // "Padding" to prevent member-specific command button from overlapping job indicator - new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null) + { + CanBeFocused = false + }; var jobIconBackground = new GUIImage( new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), @@ -320,7 +367,6 @@ namespace Barotrauma CanBeFocused = false, UserData = "job" }; - if (character?.Info?.Job.Prefab?.Icon != null) { new GUIImage( @@ -362,36 +408,6 @@ namespace Barotrauma }; nameBlock.Text = ToolBox.LimitString(character.Name, font, (int)nameBlock.Rect.Width); - var nameActualRealtiveWidth = Math.Min(nameRelativeWidth * background.Rect.Width, 150) / background.Rect.Width; - var characterButton = new GUIButton( - new RectTransform( - new Vector2(paddingRelativeWidth + 0.8f * iconRelativeWidth + nameActualRealtiveWidth + 2 * layoutGroup.RelativeSpacing, 1.0f), - background.RectTransform), - style: null) - { - UserData = character, - OnSecondaryClicked = (comp, data) => - { - if (data == null) { return false; } - if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client) - { - CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); - return true; - } - return false; - } - }; - SetCharacterButtonTooltip(characterButton); - - if (IsSinglePlayer) - { - characterButton.OnClicked = CharacterClicked; - } - else - { - characterButton.CanBeSelected = false; - } - new GUIImage( new RectTransform(new Vector2(0.1f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform), style: "VerticalLine") @@ -487,15 +503,15 @@ namespace Barotrauma }; } - private void SetCharacterButtonTooltip(GUIButton characterButton) + private void SetCharacterComponentTooltip(GUIComponent characterComponent) { - var character = (Character)characterButton.UserData; - if (character?.Info?.Job?.Prefab == null) { return; } + if (!(characterComponent?.UserData is Character character)) { return; } + if (character.Info?.Job?.Prefab == null) { return; } string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); string tooltip = $"‖color:{color}‖{character.Name} ({character.Info.Job.Name})‖color:end‖"; var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); - characterButton.ToolTip = sanitizedTooltip; - characterButton.TooltipRichTextData = richTextData; + characterComponent.ToolTip = sanitizedTooltip; + characterComponent.TooltipRichTextData = richTextData; } /// @@ -564,10 +580,18 @@ namespace Barotrauma partial void RenameCharacterProjSpecific(CharacterInfo characterInfo) { if (!(crewList.Content.GetChildByUserData(characterInfo?.Character) is GUIComponent characterComponent)) { return; } + SetCharacterComponentTooltip(characterComponent); if (!(characterComponent.FindChild("name", recursive: true) is GUITextBlock nameBlock)) { return; } nameBlock.Text = ToolBox.LimitString(characterInfo.Name, nameBlock.Font, nameBlock.Rect.Width); - if (!(characterComponent.FindChild(c => c is GUIButton && c.UserData == characterInfo?.Character) is GUIButton characterButton)) { return; } - SetCharacterButtonTooltip(characterButton); + } + + private void OnCrewListRearranged(GUIListBox crewList, object draggedElementData) + { + if (crewList != this.crewList) { return; } + if (!(draggedElementData is Character)) { return; } + if (crewList.HasDraggedElementIndexChanged) { return; } + if (!IsSinglePlayer) { return; } + CharacterClicked(crewList.DraggedElement, draggedElementData); } #endregion @@ -682,18 +706,18 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null) { if (order != null && order.TargetAllCharacters) { - Hull hull = null; + Hull hull = targetHull; if (order.IsReport) { - if (orderGiver?.CurrentHull == null) { return; } - hull = orderGiver.CurrentHull; + if (orderGiver?.CurrentHull == null && hull == null) { return; } + hull ??= orderGiver.CurrentHull; AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); } - else if(order.IsIgnoreOrder) + else if (order.IsIgnoreOrder) { WallSection ws = null; if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) @@ -748,7 +772,8 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - orderGiver?.Speak(order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option)); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); + orderGiver?.Speak(message); } else if (orderGiver != null) { @@ -1299,6 +1324,80 @@ namespace Barotrauma return 0; } + private bool CreateOrder(Order order, Hull targetHull = null) + { + var sub = Character.Controlled.Submarine; + + if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } + + SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled, targetHull); + + if (IsSinglePlayer) + { + HumanAIController.ReportProblem(Character.Controlled, order); + } + + return true; + } + + private void UpdateOrderDrag() + { + if (DraggedOrder is { } order) + { + if (dropOrder) + { + // stinky workaround + if (framesToSkip > 0) + { + framesToSkip--; + } + else + { + Hull hull = null; + + if (GUI.MouseOn is GUIFrame frame) + { + if (frame.UserData is Hull data) + { + hull = data; + } + else if (frame.Parent?.UserData is Hull parentData) + { + hull = parentData; + } + } + + framesToSkip = 2; + dropOrder = false; + DraggedOrder = null; + + if (hull is null && GUI.MouseOn is { Visible: true, CanBeFocused: true }) { return; } + + hull ??= Hull.hullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); + CreateOrder(order, hull); + } + } + else + { + DragOrder = DragOrder || Vector2.DistanceSquared(dragPoint, PlayerInput.MousePosition) > dragOrderTreshold * dragOrderTreshold; + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + if (DragOrder) + { + dropOrder = true; + } + else + { + DraggedOrder = null; + } + dragPoint = Vector2.Zero; + DragOrder = false; + } + } + } + } + partial void UpdateProjectSpecific(float deltaTime) { // Quick selection @@ -1316,6 +1415,8 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } + UpdateOrderDrag(); + #region Command UI WasCommandInterfaceDisabledThisUpdate = false; @@ -1756,7 +1857,8 @@ namespace Barotrauma } } private GUIFrame commandFrame, targetFrame; - private GUIButton centerNode, returnNode, expandNode, shortcutCenterNode; + private GUIButton centerNode, returnNode, expandNode; + private GUIFrame shortcutCenterNode; private readonly List> optionNodes = new List>(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; private readonly List shortcutNodes = new List(); @@ -1790,7 +1892,7 @@ namespace Barotrauma private bool isContextual; private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; - private const int maxShortCutNodeCount = 4; + private const int maxShortcutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } public static bool CanIssueOrders @@ -1805,13 +1907,23 @@ namespace Barotrauma } } - private bool CanSomeoneHearCharacter() + private bool CanCharacterBeHeard() { #if DEBUG if (Character.Controlled == null) { return true; } #endif - return Character.Controlled != null && - (characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled))); + if (Character.Controlled != null) + { + if (characterContext == null) + { + return characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); + } + else + { + return characterContext.CanHearCharacter(Character.Controlled); + } + } + return false; } private Entity FindEntityContext() @@ -2101,6 +2213,8 @@ namespace Barotrauma shortcutNodes.Remove(node); }; RemoveOptionNodes(); + bool wasMinimapVisible = targetFrame != null && targetFrame.Visible; + HideMinimap(); if (returnNode != null) { @@ -2111,12 +2225,9 @@ namespace Barotrauma } // When the mini map is shown, always position the return node on the bottom - List matchingItems = null; - if (node?.UserData is Order order) - { - matchingItems = order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled); - } - var offset = matchingItems != null && matchingItems.Count > 1 ? + bool placeReturnNodeOnTheBottom = wasMinimapVisible || + (node?.UserData is Order order && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).Count > 1); + var offset = placeReturnNodeOnTheBottom ? new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); SetReturnNode(centerNode, offset); @@ -2137,12 +2248,7 @@ namespace Barotrauma { if (commandFrame == null) { return false; } RemoveOptionNodes(); - if (targetFrame != null) - { - targetFrame.Visible = false; - nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; - nodeConnectors.RectTransform.RepositionChildInHierarchy(1); - } + HideMinimap(); // TODO: Center node could move to option node instead of being removed commandFrame.RemoveChild(centerNode); SetCenterNode(node); @@ -2163,6 +2269,15 @@ namespace Barotrauma return true; } + private void HideMinimap() + { + if (targetFrame == null || !targetFrame.Visible) { return; } + targetFrame.Visible = false; + // Reset the node connectors to their original parent + nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; + nodeConnectors.RectTransform.RepositionChildInHierarchy(1); + } + private void CreateReturnNodeHotkey() { if (returnNode != null && returnNode.Visible) @@ -2203,6 +2318,7 @@ namespace Barotrauma } node.OnClicked = null; node.OnSecondaryClicked = null; + node.CanBeFocused = false; centerNode = node; } @@ -2219,6 +2335,7 @@ namespace Barotrauma } node.OnClicked = NavigateBackward; node.OnSecondaryClicked = null; + node.CanBeFocused = true; returnNode = node; } @@ -2240,9 +2357,33 @@ namespace Barotrauma { CreateOrderNodes(category); } - else if (userData is Order order) + else if (userData is Order nodeOrder) { - CreateOrderOptions(order); + Submarine submarine = GetTargetSubmarine(); + List matchingItems = null; + if (itemContext == null && nodeOrder.MustSetTarget) + { + 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) + { + CreateMinimapNodes(nodeOrder, submarine, matchingItems); + } + //only one target (or an order with no particular targets), just show options + else + { + CreateOrderOptionNodes(nodeOrder, itemContext ?? nodeOrder.TargetEntity as Item ?? matchingItems?.FirstOrDefault()); + } + } + else if (userData is (Order minimapOrder, string option) && minimapOrder.HasOptions && string.IsNullOrEmpty(option)) + { + CreateOrderOptionNodes(minimapOrder, minimapOrder.TargetEntity as Item); + } + else + { + DebugConsole.ThrowError($"Unexpected node user data of type {userData.GetType()} when creating command interface nodes"); + return false; } return true; } @@ -2291,7 +2432,7 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); if (Order.OrderCategoryIcons.TryGetValue(category, out Tuple sprite)) { - var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLower()); + var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLowerInvariant()); var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); @@ -2302,99 +2443,91 @@ namespace Barotrauma private void CreateShortcutNodes() { - bool HasAppropriateJob(Character c, string jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); - - Submarine sub = GetTargetSubmarine(); - + var sub = GetTargetSubmarine(); if (sub == null) { return; } - shortcutNodes.Clear(); - - if (shortcutNodes.Count < maxShortCutNodeCount && - sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + if (CanFitMoreNodes() && sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { - var reactorOutput = -reactor.CurrPowerConsumption; + float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option - if ((Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "operatereactor")) && - reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) + if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); - var option = order.Prefab.Options[0]; - shortcutNodes.Add( - CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); - } - } - - // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it - // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up - // --> Create shortcut node for Steer order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "steer")) && - sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && - nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("steer"), -1)); - } - - // If player is not a security officer AND invaders are reported - // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fightintruders")) && - Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders)) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fightintruders"), -1)); - } - - // If player is not a mechanic AND a breach has been reported - // --> Create shorcut node for Fix Leaks order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fixleaks")) && - Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) - { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fixleaks"), -1)); - } - - // --> Create shortcut nodes for the Repair orders - if (shortcutNodes.Count < maxShortCutNodeCount && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) - { - // TODO: Doesn't work for player issued reports, because they don't have a target. - int repairNodes = 0; - string tag = "repairelectrical"; - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) - { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); - repairNodes++; - } - tag = "repairmechanical"; - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) - { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); - repairNodes++; - } - if (repairNodes == 0 && shortcutNodes.Count < maxShortCutNodeCount) - { - tag = "repairsystems"; - if (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) + string option = order.Prefab.Options[0]; + if (IsNonDuplicateOrder(order, option)) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); + shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); } } } - + // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it + // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up + // --> Create shortcut node for Steer order + if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && Order.GetPrefab("steer") is Order steerOrder && IsNonDuplicateOrder(steerOrder) && + sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, steerOrder, -1)); + } + // If player is not a security officer AND invaders are reported + // --> Create shorcut node for Fight Intruders order + if (CanFitMoreNodes() && ShouldDelegateOrder("fightintruders") && + Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders) && + Order.GetPrefab("fightintruders") is Order fightOrder && IsNonDuplicateOrder(fightOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fightOrder, -1)); + } + // If player is not a mechanic AND a breach has been reported + // --> Create shorcut node for Fix Leaks order + if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && Order.GetPrefab("fixleaks") is Order fixLeaksOrder && IsNonDuplicateOrder(fixLeaksOrder) && + Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fixLeaksOrder, -1)); + } + // --> Create shortcut nodes for the Repair orders + if (CanFitMoreNodes() && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) + { + // TODO: Doesn't work for player issued reports, because they don't have a target. + bool useSpecificRepairOrder = false; + string tag = "repairelectrical"; + if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + { + if (Order.GetPrefab(tag) is Order repairElectricalOrder && IsNonDuplicateOrder(repairElectricalOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairElectricalOrder, -1)); + } + useSpecificRepairOrder = true; + } + tag = "repairmechanical"; + if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + { + if (Order.GetPrefab(tag) is Order repairMechanicalOrder && IsNonDuplicateOrder(repairMechanicalOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairMechanicalOrder, -1)); + } + useSpecificRepairOrder = true; + } + tag = "repairsystems"; + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && Order.GetPrefab(tag) is Order repairOrder && IsNonDuplicateOrder(repairOrder)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairOrder, -1)); + } + } // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (shortcutNodes.Count < maxShortCutNodeCount && ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) + if (CanFitMoreNodes() && Order.GetPrefab("extinguishfires") is Order extinguishOrder && IsNonDuplicateOrder(extinguishOrder) && + ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, extinguishOrder, -1)); } - - if (shortcutNodes.Count < maxShortCutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) + if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { - if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && + if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && IsNonDuplicateOrder(orderPrefab) && shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && !orderPrefab.IsReport && orderPrefab.Category != null) @@ -2403,22 +2536,19 @@ namespace Barotrauma { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } - if (shortcutNodes.Count >= maxShortCutNodeCount) { break; } + if (!CanFitMoreNodes()) { break; } } } } - - if (shortcutNodes.Count < maxShortCutNodeCount && characterContext != null && !characterContext.IsDismissed) + if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); } - if (shortcutNodes.Count < 1) { return; } - - shortcutCenterNode = new GUIButton( - new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), - style: null); + shortcutCenterNode = new GUIFrame(new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) + { + CanBeFocused = false + }; CreateNodeIcon(shortcutCenterNode.RectTransform, "CommandShortcutNode"); foreach (GUIComponent c in shortcutCenterNode.Children) { @@ -2427,15 +2557,29 @@ namespace Barotrauma c.SelectedColor = c.Color; } shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration); - - var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); - var firstOffsetIndex = nodeCountForCalculations / 2 - 1; + int nodeCountForCalculations = shortcutNodes.Count * 2 + 2; + Vector2[] offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); + int firstOffsetIndex = nodeCountForCalculations / 2 - 1; for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } + + bool CanFitMoreNodes() + { + return shortcutNodes.Count < maxShortcutNodeCount; + } + static bool ShouldDelegateOrder(string orderIdentifier) + { + return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); + } + bool IsNonDuplicateOrder(Order orderPrefab, string option = null) + { + return characterContext == null || (string.IsNullOrEmpty(option) ? + characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier) : + characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier && oi.OrderOption == option)); + } } private void CreateOrderNodes(OrderCategory orderCategory) @@ -2448,7 +2592,7 @@ namespace Barotrauma for (int i = 0; i < orders.Count; i++) { order = orders[i]; - disableNode = !CanSomeoneHearCharacter() || + disableNode = !CanCharacterBeHeard() || (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); optionNodes.Add(new Tuple( @@ -2596,7 +2740,7 @@ namespace Barotrauma } var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); - bool disableNode = !CanSomeoneHearCharacter(); + bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) { optionNodes.Add(new Tuple( @@ -2621,6 +2765,7 @@ namespace Barotrauma item.GetConnectedComponents(recursive: true).Any(c => c.Item.HasTag(operateWeaponsPrefab.TargetItems))); } + /// 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) { var node = new GUIButton( @@ -2633,7 +2778,7 @@ namespace Barotrauma if (checkIfOrderCanBeHeard && !disableNode) { - disableNode = !CanSomeoneHearCharacter(); + disableNode = !CanCharacterBeHeard(); } var mustSetOptionOrTarget = order.HasOptions; @@ -2694,7 +2839,7 @@ namespace Barotrauma if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear")); } else if (hotkey >= 0) { @@ -2703,195 +2848,137 @@ namespace Barotrauma return node; } - private void CreateOrderOptions(Order order) + private void CreateMinimapNodes(Order order, Submarine submarine, List matchingItems) { - Submarine submarine = GetTargetSubmarine(); - var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled) : new List(); - - //more than one target item -> create a minimap-like selection with a pic of the sub - if (itemContext == null && matchingItems.Count > 1) + // TODO: Further adjustments to frameSize calculations + // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much + Point frameSize; + Rectangle subBorders = submarine.GetDockedBorders(); + if (subBorders.Width > subBorders.Height) { - // TODO: Further adjustments to frameSize calculations - // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much - Point frameSize; - Rectangle subBorders = submarine.GetDockedBorders(); - if (subBorders.Width > subBorders.Height) + frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2; + //height depends on the dimensions of the sub + frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width)); + } + else + { + frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2; + //width depends on the dimensions of the sub + frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height)); + } + + // TODO: Use the old targetFrame if possible + targetFrame = new GUIFrame( + new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { - frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2; - //height depends on the dimensions of the sub - frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width)); - } - else + AbsoluteOffset = new Point(0, -150), + Pivot = Pivot.BottomCenter + }, + style: "InnerFrame"); + + submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems); + + new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay) + { + CanBeFocused = false, + UserData = submarine + }; + + List optionElements = new List(); + foreach (Item item in matchingItems) + { + var itemTargetFrame = targetFrame.Children.First().FindChild(item); + if (itemTargetFrame == null) { continue; } + + var anchor = Anchor.TopLeft; + if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f) { - frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2; - //width depends on the dimensions of the sub - frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height)); - } - - // TODO: Use the old targetFrame if possible - targetFrame = new GUIFrame( - new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) + if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) { - AbsoluteOffset = new Point(0, -150), - Pivot = Pivot.BottomCenter - }, - style: "InnerFrame"); - - submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems); - - new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay) - { - CanBeFocused = false, - UserData = submarine - }; - - List optionElements = new List(); - foreach (Item item in matchingItems) - { - var itemTargetFrame = targetFrame.Children.First().FindChild(item); - if (itemTargetFrame == null) { continue; } - - var anchor = Anchor.TopLeft; - if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f) - { - if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) - { - anchor = Anchor.BottomRight; - } - else - { - anchor = Anchor.TopRight; - } - } - else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) - { - anchor = Anchor.BottomLeft; - } - - GUIComponent optionElement; - if (order.Options.Length > 1) - { - optionElement = new GUIFrame( - new RectTransform( - new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: "InnerFrame"); - - new GUIFrame( - new RectTransform(Vector2.One, optionElement.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: Color.Black * 0.7f); - - var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionElement.RectTransform, anchor: Anchor.Center)) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), - item?.Name ?? GetOrderNameBasedOnContextuality(order)); - - for (int i = 0; i < order.Options.Length; i++) - { - var optionButton = new GUIButton( - new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), - text: order.GetOptionName(i), style: "GUITextBox") - { - UserData = new Tuple( - item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), - order.Options[i]), - Font = GUI.SmallFont, - OnClicked = (_, userData) => - { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - DisableCommandUI(); - return true; - } - }; - if (CanOpenManualAssignment(optionButton)) - { - optionButton.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); - } - optionNodes.Add(new Tuple(optionButton, Keys.None)); - } + anchor = Anchor.BottomRight; } else { - var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); - optionElement = new GUIButton( - new RectTransform( - new Point((int)(50 * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: null) - { - UserData = userData, - Font = GUI.SmallFont, - OnClicked = (_, userData) => - { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); - DisableCommandUI(); - return true; - } - }; - if (CanOpenManualAssignment(optionElement)) - { - optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); - } - var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && - o.Order.Identifier == userData.Item1.Identifier && - o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; - CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); - optionNodes.Add(new Tuple(optionElement, Keys.None)); + anchor = Anchor.TopRight; } - optionElements.Add(optionElement); + } + else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) + { + anchor = Anchor.BottomLeft; } - Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20); - if (order.Identifier == "operateweapons") + var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); + var optionElement = new GUIButton( + new RectTransform( + new Point((int)(50 * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: null) { - Rectangle disallowedArea = targetFrame.GetChild().Rect; - Point originalSize = disallowedArea.Size; - disallowedArea.Size = disallowedArea.MultiplySize(0.9f); - disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2; - disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2; - GUI.PreventElementOverlap(optionElements, new List() { disallowedArea }, clampArea); - nodeConnectors.RectTransform.Parent = targetFrame.RectTransform; - nodeConnectors.RectTransform.SetAsFirstChild(); - } - else + UserData = userData, + Font = GUI.SmallFont, + OnClicked = (button, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + if (o.Item1.HasOptions) + { + NavigateForward(button, userData); + } + else + { + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + DisableCommandUI(); + } + return true; + } + }; + if (CanOpenManualAssignment(optionElement)) { - GUI.PreventElementOverlap(optionElements, clampArea: clampArea); + optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - - var shadow = new GUIFrame( - new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); - shadow.SetAsFirstChild(); + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && + o.Order.Identifier == userData.Item1.Identifier && + o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); + optionNodes.Add(new Tuple(optionElement, Keys.None)); + optionElements.Add(optionElement); } - //only one target (or an order with no particular targets), just show options - else + + Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20); + Rectangle disallowedArea = targetFrame.GetChild().Rect; + Point originalSize = disallowedArea.Size; + disallowedArea.Size = disallowedArea.MultiplySize(0.9f); + disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2; + disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2; + GUI.PreventElementOverlap(optionElements, new List() { disallowedArea }, clampArea); + nodeConnectors.RectTransform.Parent = targetFrame.RectTransform; + nodeConnectors.RectTransform.SetAsFirstChild(); + + var shadow = new GUIFrame( + new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), + style: "OuterGlow", + color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); + shadow.SetAsFirstChild(); + } + + private void CreateOrderOptionNodes(Order order, Item targetItem) + { + if (itemContext != null) { - var item = itemContext != null ? - (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : - (matchingItems.Count > 0 ? matchingItems[0] : null); - var o = item == null || !order.IsPrefab ? order : new Order(order, item, order.GetTargetItemComponent(item)); - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, - GetCircumferencePointCount(order.Options.Length), - GetFirstNodeAngle(order.Options.Length)); - var offsetIndex = 0; - for (int i = 0; i < order.Options.Length; i++) - { - optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), - Keys.D0 + (i + 1) % 10)); - } + targetItem = !order.UseController ? itemContext : + itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item; + } + var o = (targetItem == null || !order.IsPrefab) ? order : new Order(order, targetItem, order.GetTargetItemComponent(targetItem)); + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, + GetCircumferencePointCount(order.Options.Length), + GetFirstNodeAngle(order.Options.Length)); + var offsetIndex = 0; + for (int i = 0; i < order.Options.Length; i++) + { + optionNodes.Add(new Tuple( + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), + Keys.D0 + (i + 1) % 10)); } } @@ -2924,11 +3011,11 @@ namespace Barotrauma "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); } - if (!CanSomeoneHearCharacter()) + if (!CanCharacterBeHeard()) { node.CanBeFocused = false; if (icon != null) { icon.CanBeFocused = false; } - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear")); } else if (hotkey >= 0) { @@ -2990,9 +3077,7 @@ namespace Barotrauma SetCenterNode(clickedOptionNode); node = null; } - targetFrame.Visible = false; - nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; - nodeConnectors.RectTransform.RepositionChildInHierarchy(1); + HideMinimap(); } if (shortcutCenterNode != null) { @@ -3178,7 +3263,7 @@ namespace Barotrauma if (!canHear) { node.CanBeFocused = orderIcon.CanBeFocused = false; - CreateBlockIcon(node.RectTransform); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("thischaractercanthear")); } if (hotkey >= 0) { @@ -3270,14 +3355,23 @@ namespace Barotrauma }; } - private void CreateBlockIcon(RectTransform parent) + private void CreateBlockIcon(RectTransform parent, string tooltip = null) { - new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) + var icon = new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { CanBeFocused = false, Color = GUI.Style.Red * nodeColorMultiplier, HoverColor = GUI.Style.Red }; + if (!string.IsNullOrEmpty(tooltip)) + { + icon.ToolTip = tooltip; + string color = XMLExtensions.ColorToString(GUI.Style.Red); + tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; + var richTextData = RichTextData.GetRichTextData(tooltip, out _); + icon.TooltipRichTextData = richTextData; + icon.CanBeFocused = true; + } } private int GetCircumferencePointCount(int nodes) @@ -3383,15 +3477,15 @@ namespace Barotrauma private bool CanOpenManualAssignment(GUIComponent node) { if (node == null || characterContext != null) { return false; } - if (node.UserData is Tuple orderInfo) + if (node.UserData is (Order minimapOrder, string option)) { - return !orderInfo.Item1.TargetAllCharacters; + return !minimapOrder.TargetAllCharacters && (!minimapOrder.HasOptions || !string.IsNullOrEmpty(option)); } - if (node.UserData is Order order) + if (node.UserData is Order nodeOrder) { - return !order.TargetAllCharacters && !order.HasOptions && - (!order.MustSetTarget || itemContext != null || - order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + return !nodeOrder.TargetAllCharacters && !nodeOrder.HasOptions && + (!nodeOrder.MustSetTarget || itemContext != null || + nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index e2a74ba9a..32605d848 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -60,7 +60,7 @@ namespace Barotrauma var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); - GameMain.NetLobbyScreen.CampaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, null, saveFiles); + GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, null, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") @@ -761,7 +761,7 @@ namespace Barotrauma Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); if (faction?.Reputation != null) { - faction.Reputation.Value = rep; + faction.Reputation.SetReputation(rep); } else { @@ -771,7 +771,7 @@ namespace Barotrauma if (reputation.HasValue) { - campaign.Map.CurrentLocation.Reputation.Value = reputation.Value; + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 90aae527a..b5246980c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -21,10 +21,11 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetLobbyScreen != null) { - if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.Dispose(); + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu = null; if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } - if (tabMenu == null && GameMode is TutorialMode == false) + if (tabMenu == null && !(GameMode is TutorialMode) && !ConversationAction.IsDialogOpen) { tabMenu = new TabMenu(); HintManager.OnShowTabMenu(); @@ -34,17 +35,17 @@ namespace Barotrauma tabMenu = null; NetLobbyScreen.JobInfoFrame = null; } - return true; } private GUILayoutGroup topLeftButtonGroup; private GUIButton crewListButton, commandButton, tabMenuButton; + private GUIImage talentPointNotification; private GUIComponent respawnInfoFrame, respawnButtonContainer; private GUITextBlock respawnInfoText; private GUITickBox respawnTickBox; - private GUILayoutGroup TopLeftButtonGroup; + private void CreateTopLeftButtons() { if (topLeftButtonGroup != null) @@ -89,11 +90,11 @@ namespace Barotrauma tabMenuButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "TabMenuButton") { ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameMain.Config.KeyBindText(InputType.InfoTab)), - OnClicked = (button, userData) => - { - return ToggleTabMenu(); - } + OnClicked = (button, userData) => ToggleTabMenu() }; + + talentPointNotification = CreateTalentIconNotification(tabMenuButton); + GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) @@ -140,11 +141,41 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { - GameMain.NetLobbyScreen?.HeadSelectionList?.AddToGUIUpdateList(); + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(); } } + public static GUIImage CreateTalentIconNotification(GUIComponent parent, bool offset = true) + { + GUIImage indicator = new GUIImage(new RectTransform(new Vector2(0.45f), parent.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.BothWidth), style: "TalentPointNotification") + { + Visible = false, + CanBeFocused = false + }; + Point notificationSize = indicator.RectTransform.NonScaledSize; + if (offset) + { + indicator.RectTransform.AbsoluteOffset = new Point(-(notificationSize.X / 2), -(notificationSize.Y / 2)); + } + return indicator; + } + + public static void UpdateTalentNotificationIndicator(GUIImage indicator) + { + if (indicator != null) + { + if (Character.Controlled?.Info == null) + { + indicator.Visible = false; + } + else + { + indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0; + } + } + } + partial void UpdateProjSpecific(float deltaTime) { if (GUI.DisableHUD) { return; } @@ -159,28 +190,24 @@ namespace Barotrauma else { tabMenu.Update(); - if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && + if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { ToggleTabMenu(); } } + UpdateTalentNotificationIndicator(talentPointNotification); + if (GameMain.NetworkMember != null) { - if (GameMain.NetLobbyScreen?.HeadSelectionList != null) - { - if (PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.HeadSelectionList)) - { - if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } - } - } + GameMain.NetLobbyScreen?.CharacterAppearanceCustomizationMenu?.Update(); if (GameMain.NetLobbyScreen?.JobSelectionFrame != null) { - if (PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.JobSelectionFrame)) + if (GameMain.NetLobbyScreen.JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.JobSelectionFrame)) { GameMain.NetLobbyScreen.JobList.Deselect(); - if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } + GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index be0df2d3e..b58477409 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -544,7 +544,7 @@ namespace Barotrauma if (Character.Controlled.CurrentHull == null) { return; } if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull)) { - if (DisplayHint("onballastflorainfected")) { return; } + if (IsOnFriendlySub() && DisplayHint("onballastflorainfected")) { return; } } foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) { @@ -552,7 +552,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(Character.Controlled.WorldPosition, gap.ConnectedDoor.Item.WorldPosition) > 400 * 400) { continue; } if (!gap.IsRoomToRoom) { - if (!(Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item)) { continue; } + if (!IsWearingDivingSuit()) { continue; } if (Character.Controlled.IsProtectedFromPressure()) { continue; } if (DisplayHint("divingsuitwarning", extendTextTag: false)) { return; } continue; @@ -561,10 +561,16 @@ namespace Barotrauma { if (me == Character.Controlled.CurrentHull) { continue; } if (!(me is Hull adjacentHull)) { continue; } + if (!IsOnFriendlySub()) { continue; } + if (IsWearingDivingSuit()) { continue; } if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure")) { return; } if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage")) { return; } } + + static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item; } + + static bool IsOnFriendlySub() => Character.Controlled.Submarine is Submarine sub && (sub.TeamID == Character.Controlled.TeamID || sub.TeamID == CharacterTeamType.FriendlyNPC); } private static void CheckReminders() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 200d39720..49b528e7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -51,7 +51,7 @@ namespace Barotrauma { keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.R); + keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl); keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G); keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H); @@ -173,7 +173,7 @@ namespace Barotrauma } } - private void LoadKeyBinds(XElement element) + private void LoadKeyBinds(XElement element, Version gameVersion) { foreach (XAttribute attribute in element.Attributes()) { @@ -183,7 +183,6 @@ namespace Barotrauma keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); } - if (!Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) { continue; } if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) @@ -199,6 +198,13 @@ namespace Barotrauma keyMapping[(int)inputType] = new KeyOrMouse(key); } } + //v0.15 added creature attacks that can be used with a character capable of speaking (with mudraptor or spineling genes), + //which causes the previous attack keybind R to conflict with the radio keybind + // -> automatically change it to F + if (gameVersion < new Version(0, 15, 0, 0)) + { + keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); + } } private void LoadInventoryKeybinds(XElement element) @@ -223,10 +229,12 @@ namespace Barotrauma private void LoadControls(XDocument doc) { + var gameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); + XElement keyMapping = doc.Root.Element("keymapping"); if (keyMapping != null) { - LoadKeyBinds(keyMapping); + LoadKeyBinds(keyMapping, gameVersion); } XElement inventoryKeyMapping = doc.Root.Element("inventorykeymapping"); @@ -1518,6 +1526,12 @@ namespace Barotrauma "Automatic quickstart enabled", "Will the game automatically move on to Quickstart when the game is launched"); + addDebugTickBox( + TestScreenEnabled, + (b) => TestScreenEnabled = b, + "Test screen enabled", + "Will the game automatically move on to a test screen when the game is launched"); + addDebugTickBox( AutomaticCampaignLoadEnabled, (b) => AutomaticCampaignLoadEnabled = b, @@ -1831,6 +1845,7 @@ namespace Barotrauma ic.ParseMsg(); } } + CharacterHUD.ShouldRecreateHudTexts = true; } private void ApplySettings() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 516f62488..2e3782d15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -161,7 +161,7 @@ namespace Barotrauma public override void CreateSlots() { - if (visualSlots == null) { visualSlots = new VisualSlot[capacity]; } + visualSlots ??= new VisualSlot[capacity]; float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; @@ -174,6 +174,12 @@ namespace Barotrauma (int)SlotPositions[i].X, (int)SlotPositions[i].Y, (int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier)); + + if (SlotTypes[i] == InvSlotType.HealthInterface && + character.CharacterHealth?.InventorySlotContainer != null) + { + slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f); + } ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent(); if (itemContainer != null) @@ -238,6 +244,8 @@ namespace Barotrauma { if (visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty())) { return true; } + if (CharacterHealth.OpenHealthWindow != Character.Controlled?.CharacterHealth && SlotTypes[i] == InvSlotType.HealthInterface) { return true; } + if (layout == Layout.Default) { if (PersonalSlots.HasFlag(SlotTypes[i]) && !personalSlotArea.Contains(visualSlots[i].Rect.Center + visualSlots[i].DrawOffset.ToPoint())) { return true; } @@ -315,7 +323,7 @@ namespace Barotrauma case Layout.Default: { int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); - int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s)); + int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; @@ -359,7 +367,8 @@ namespace Barotrauma int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; for (int i = 0; i < visualSlots.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { //upperX -= slotSize.X + spacing; @@ -371,10 +380,18 @@ namespace Barotrauma } int lowerX = x; + int handSlotX = x; int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) + { + SlotPositions[i] = new Vector2(handSlotX, personalSlotY); + handSlotX += visualSlots[i].Rect.Width + Spacing; + continue; + } + + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); @@ -390,7 +407,8 @@ namespace Barotrauma x = lowerX; for (int i = 0; i < SlotPositions.Length; i++) { - if (!HideSlot(i)) continue; + if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } x -= visualSlots[i].Rect.Width + Spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } @@ -404,7 +422,8 @@ namespace Barotrauma for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); @@ -416,9 +435,16 @@ namespace Barotrauma x += visualSlots[i].Rect.Width + Spacing; } } + int handSlotX = x - visualSlots[0].Rect.Width - Spacing; for (int i = 0; i < SlotPositions.Length; i++) { - if (!HideSlot(i)) continue; + if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) + { + bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + continue; + } + if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); x += visualSlots[i].Rect.Width + Spacing; } @@ -432,7 +458,7 @@ namespace Barotrauma int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); @@ -444,7 +470,7 @@ namespace Barotrauma int n = 0; for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); @@ -461,13 +487,23 @@ namespace Barotrauma } break; } - + + if (character.CharacterHealth?.UseHealthWindow ?? false) + { + Vector2 pos = character.CharacterHealth.InventorySlotContainer.Rect.Location.ToVector2(); + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } + SlotPositions[i] = pos; + pos.Y += visualSlots[i].Rect.Height + Spacing; + } + } + CreateSlots(); if (layout == Layout.Default) { HUDLayoutSettings.InventoryTopY = visualSlots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); } - } protected override void ControlInput(Camera cam) @@ -652,6 +688,11 @@ namespace Barotrauma { break; } + //if putting an item to a container with a max stack size of 1, only put one item from the stack + if (quickUseAction == QuickUseAction.PutToContainer && (character.SelectedConstruction?.GetComponent()?.MaxStackSize ?? 0) <= 1) + { + break; + } } } @@ -661,7 +702,7 @@ namespace Barotrauma if (item != null) { var slot = visualSlots[i]; - if (item.AllowedSlots.Any(a => a != InvSlotType.Any)) + if (item.AllowedSlots.Any(a => a != InvSlotType.Any && a != InvSlotType.HealthInterface)) { HandleButtonEquipStates(item, slot, deltaTime); } @@ -840,7 +881,9 @@ namespace Barotrauma private QuickUseAction GetQuickUseAction(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) { - if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null) + if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null && + //if the item can be equipped in the health interface slot, don't use it as a treatment but try to equip it + !item.AllowedSlots.Contains(InvSlotType.HealthInterface)) { return QuickUseAction.UseTreatment; } @@ -1135,7 +1178,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (HideSlot(i)) { continue; } + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } //don't draw the item if it's being dragged out of the slot bool drawItem = !DraggingItems.Any() || !slots[i].Items.All(it => DraggingItems.Contains(it)) || visualSlots[i].MouseOn(); @@ -1189,7 +1232,7 @@ namespace Barotrauma highlightedQuickUseSlot = visualSlots[i]; } - if (!slots[i].First().AllowedSlots.Any(a => a == InvSlotType.Any)) + if (slots[i].First().AllowedSlots.Count() == 1 || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs new file mode 100644 index 000000000..cd8d21dcf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Items.Components +{ + internal partial class EntitySpawnerComponent + { + public Vector2 DrawSize => Vector2.Zero; + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + if (!editing) { return; } + + switch (SpawnAreaShape) + { + case AreaShape.Rectangle: + { + RectangleF rect = GetAreaRectangle(SpawnAreaBounds, SpawnAreaOffset, draw: true); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 4f); + + if (MaximumAmountRangePadding > 0f) + { + rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 2f); + } + break; + } + case AreaShape.Circle: + Vector2 center = item.WorldPosition; + center.Y = -center.Y; + center += SpawnAreaOffset; + spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUI.Style.Red, thickness: 4f); + + if (MaximumAmountRangePadding > 0f) + { + spriteBatch.DrawCircle(center, SpawnAreaRadius + MaximumAmountRangePadding, 32, GUI.Style.Red, thickness: 2f); + } + break; + } + + if (!OnlySpawnWhenCrewInRange) { return; } + + switch (CrewAreaShape) + { + case AreaShape.Rectangle: + { + RectangleF rect = GetAreaRectangle(CrewAreaBounds, CrewAreaOffset, draw: true); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Green, isFilled: false, 0f, 4f); + break; + } + case AreaShape.Circle: + Vector2 center = item.WorldPosition; + center.Y = -center.Y; + center += CrewAreaOffset; + spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUI.Style.Green); + break; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..174fba2b0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,86 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Linq; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent + { + [Serialize(0.0f, false)] + public float TooltipValueMin { get; set; } + + [Serialize(0.0f, false)] + public float TooltipValueMax { get; set; } + + public override void AddTooltipInfo(ref string name, ref string description) + { + if (!string.IsNullOrEmpty(materialName) && item.ContainedItems.Count() > 0) + { + string mergedMaterialName = materialName; + foreach (Item containedItem in item.ContainedItems) + { + var containedMaterial = containedItem.GetComponent(); + if (containedMaterial == null) { continue; } + mergedMaterialName += ", " + containedMaterial.materialName; + } + name = name.Replace(materialName, mergedMaterialName); + } + + if (Tainted) + { + name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); + } + + if (TextManager.ContainsTag("entitydescription." + Item.prefab.Identifier)) + { + int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f); + description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString()); + } + foreach (Item containedItem in item.ContainedItems) + { + var containedGeneticMaterial = containedItem.GetComponent(); + if (containedGeneticMaterial == null) { continue; } + string _ = string.Empty; + string containedDescription = containedItem.Description; + containedGeneticMaterial.AddTooltipInfo(ref _, ref containedDescription); + if (!string.IsNullOrEmpty(containedDescription)) + { + description += '\n' + containedDescription; + } + } + } + + public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText) + { + if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) + { + if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.prefab == item.prefab)) + { + buttonText = TextManager.Get("researchstation.combine"); + infoText = TextManager.Get("researchstation.combine.infotext"); + } + else + { + buttonText = TextManager.Get("researchstation.refine"); + int taintedProbability = (int)(GetTaintedProbabilityOnRefine(Character.Controlled) * 100); + infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString()); + } + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Tainted = msg.ReadBoolean(); + if (Tainted) + { + uint selectedTaintedEffectId = msg.ReadUInt32(); + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedTaintedEffectId); + } + else + { + uint selectedEffectId = msg.ReadUInt32(); + selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedEffectId); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index 69bc06a85..ebd3e27a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -1,13 +1,201 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.IO; namespace Barotrauma.Items.Components { partial class IdCard { - public Sprite StoredPortrait; - public Vector2 StoredSheetIndex; - public JobPrefab StoredJobPrefab; - public List StoredAttachments; + public struct OwnerAppearance + { + public Sprite Portrait; + public Vector2 SheetIndex; + public JobPrefab JobPrefab; + public List Attachments; + public Color HairColor; + public Color FacialHairColor; + public Color SkinColor; + + public void ExtractJobPrefab(string[] tags) + { + string jobIdTag = tags.FirstOrDefault(s => s.StartsWith("jobid:")); + + if (jobIdTag != null && jobIdTag.Length > 6) + { + string jobId = jobIdTag.Substring(6); + if (jobId != string.Empty) + { + JobPrefab = JobPrefab.Get(jobId); + } + } + } + + public void ExtractAppearance(CharacterInfo characterInfo, string[] tags) + { + Gender disguisedGender = Gender.None; + Race disguisedRace = Race.None; + int disguisedHeadSpriteId = -1; + int disguisedHairIndex = -1; + int disguisedBeardIndex = -1; + int disguisedMoustacheIndex = -1; + int disguisedFaceAttachmentIndex = -1; + Color hairColor = Color.Black; + Color facialHairColor = Color.Black; + Color skinColor = Color.Black; + + foreach (string tag in tags) + { + string[] s = tag.Split(':'); + + switch (s[0].ToLowerInvariant()) + { + case "haircolor": + hairColor = XMLExtensions.ParseColor(s[1]); + break; + + case "facialhaircolor": + facialHairColor = XMLExtensions.ParseColor(s[1]); + break; + + case "skincolor": + skinColor = XMLExtensions.ParseColor(s[1]); + break; + + case "gender": + Enum.TryParse(s[1], ignoreCase: true, out disguisedGender); + break; + + case "race": + Enum.TryParse(s[1], ignoreCase: true, out disguisedRace); + break; + + case "headspriteid": + int.TryParse(s[1], NumberStyles.Any, CultureInfo.InvariantCulture, out disguisedHeadSpriteId); + break; + + case "hairindex": + disguisedHairIndex = int.Parse(s[1]); + break; + + case "beardindex": + disguisedBeardIndex = int.Parse(s[1]); + break; + + case "moustacheindex": + disguisedMoustacheIndex = int.Parse(s[1]); + break; + + case "faceattachmentindex": + disguisedFaceAttachmentIndex = int.Parse(s[1]); + break; + + case "sheetindex": + string[] vectorValues = s[1].Split(";"); + SheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); + break; + } + } + + if ((characterInfo.HasGenders && disguisedGender == Gender.None) + || (characterInfo.HasRaces && disguisedRace == Race.None) + || disguisedHeadSpriteId <= 0) + { + Portrait = null; + Attachments = null; + return; + } + + foreach (XElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) + { + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + + XElement spriteElement = limbElement.Element("sprite"); + if (spriteElement == null) { continue; } + + string spritePath = spriteElement.Attribute("texture").Value; + + spritePath = spritePath.Replace("[GENDER]", disguisedGender.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[RACE]", disguisedRace.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId.ToString()); + + string fileName = Path.GetFileNameWithoutExtension(spritePath); + + //go through the files in the directory to find a matching sprite + foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) + { + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string fileWithoutTags = Path.GetFileNameWithoutExtension(file); + fileWithoutTags = fileWithoutTags.Split('[', ']').First(); + if (fileWithoutTags != fileName) { continue; } + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; + } + + break; + } + + if (characterInfo.Wearables != null) + { + float baldnessChance = disguisedGender == Gender.Female ? 0.05f : 0.2f; + + List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) + => CharacterInfo.AddEmpty( + characterInfo.FilterByTypeAndHeadID( + characterInfo.FilterElementsByGenderAndRace(characterInfo.Wearables, disguisedGender, disguisedRace), + wearableType, disguisedHeadSpriteId), + wearableType, emptyCommonness); + + var disguisedHairs = createElementList(WearableType.Hair, baldnessChance); + var disguisedBeards = createElementList(WearableType.Beard); + var disguisedMoustaches = createElementList(WearableType.Moustache); + var disguisedFaceAttachments = createElementList(WearableType.FaceAttachment); + + XElement getElementFromList(List list, int index) + => CharacterInfo.IsValidIndex(index, list) + ? list[index] + : characterInfo.GetRandomElement(list); + + var disguisedHairElement = getElementFromList(disguisedHairs, disguisedHairIndex); + var disguisedBeardElement = getElementFromList(disguisedBeards, disguisedBeardIndex); + var disguisedMoustacheElement = getElementFromList(disguisedMoustaches, disguisedMoustacheIndex); + var disguisedFaceAttachmentElement = getElementFromList(disguisedFaceAttachments, disguisedFaceAttachmentIndex); + + Attachments = new List(); + + void loadAttachments(List attachments, XElement element, WearableType wearableType) + { + foreach (var s in element?.Elements("sprite") ?? Enumerable.Empty()) + { + attachments.Add(new WearableSprite(s, wearableType)); + } + } + + loadAttachments(Attachments, disguisedFaceAttachmentElement, WearableType.FaceAttachment); + loadAttachments(Attachments, disguisedBeardElement, WearableType.Beard); + loadAttachments(Attachments, disguisedMoustacheElement, WearableType.Moustache); + loadAttachments(Attachments, disguisedHairElement, WearableType.Hair); + + loadAttachments(Attachments, + characterInfo.OmitJobInPortraitClothing + ? JobPrefab.NoJobElement?.Element("PortraitClothing") + : JobPrefab?.ClothingElement, + WearableType.JobIndicator); + } + + HairColor = hairColor; + FacialHairColor = facialHairColor; + SkinColor = skinColor; + } + } + + public OwnerAppearance StoredOwnerAppearance = default; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index ebe4c63e5..b6eca6599 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Text; using System.Xml.Linq; +using Barotrauma.Sounds; +using System.Linq; namespace Barotrauma.Items.Components { @@ -18,7 +20,11 @@ namespace Barotrauma.Items.Components protected float currentCrossHairScale, currentCrossHairPointerScale; + private RoundSound chargeSound; + private SoundChannel chargeSoundChannel; + private readonly List particleEmitters = new List(); + private readonly List particleEmitterCharges = new List(); [Serialize(1.0f, false, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale @@ -48,6 +54,12 @@ namespace Barotrauma.Items.Components case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); break; + case "particleemittercharge": + particleEmitterCharges.Add(new ParticleEmitter(subElement)); + break; + case "chargesound": + chargeSound = Submarine.LoadRoundSound(subElement, false); + break; } } } @@ -84,15 +96,62 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } + partial void UpdateProjSpecific(float deltaTime) + { + float chargeRatio = currentChargeTime / MaxChargeTime; + + switch (currentChargingState) + { + case ChargingState.WindingUp: + case ChargingState.WindingDown: + Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); + float sizeMultiplier = Math.Clamp(chargeRatio, 0.1f, 1f); + foreach (ParticleEmitter emitter in particleEmitterCharges) + { + emitter.Emit(deltaTime, particlePos, hullGuess: null, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); + } + + if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying) + { + if (chargeSound != null) + { + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); + if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + } + } + else if (chargeSoundChannel != null) + { + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + } + break; + default: + if (chargeSoundChannel != null) + { + if (chargeSoundChannel.IsPlaying) + { + chargeSoundChannel.FadeOutAndDispose(); + chargeSoundChannel.Looping = false; + } + else + { + chargeSoundChannel = null; + } + } + break; + } + } + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (character == null || !character.IsKeyDown(InputType.Aim)) { return; } //camera focused on some other item/device, don't draw the crosshair - if (character.ViewTarget != null && (character.ViewTarget is Item item) && item.Prefab.FocusOnSelected) { return; } + if (character.ViewTarget != null && (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; + GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; if (GUI.HideCursor) { crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index cc6ef2654..cb663cdcf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -246,6 +246,7 @@ namespace Barotrauma.Items.Components public void PlaySound(ActionType type, Character user = null) { if (!hasSoundsOfType[(int)type]) { return; } + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } if (loopingSound != null) { @@ -429,7 +430,7 @@ namespace Barotrauma.Items.Components } foreach (ItemComponent component in item.Components) { - if (component.name.ToLower() == LinkUIToComponent.ToLower()) + if (component.name.Equals(LinkUIToComponent, StringComparison.OrdinalIgnoreCase)) { linkToUIComponent = component; } @@ -443,9 +444,9 @@ namespace Barotrauma.Items.Components public virtual void DrawHUD(SpriteBatch spriteBatch, Character character) { } - public virtual void AddToGUIUpdateList() + public virtual void AddToGUIUpdateList(int order = 0) { - GuiFrame?.AddToGUIUpdateList(); + GuiFrame?.AddToGUIUpdateList(order: order); } public virtual void UpdateHUD(Character character, float deltaTime, Camera cam) { } @@ -620,6 +621,6 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } - public virtual void AddTooltipInfo(ref string description) { } + public virtual void AddTooltipInfo(ref string name, ref string description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 4da1bb77e..e087b99fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Items.Components /// private float[] containedSpriteDepths; + private Sprite[] slotIcons; + public Sprite InventoryTopSprite { get { return inventoryTopSprite; } @@ -58,6 +60,9 @@ namespace Barotrauma.Items.Components [Serialize(null, false)] public string ContainedStateIndicatorStyle { get; set; } + [Serialize(-1, false, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] + public int ContainedStateIndicatorSlot { get; set; } + [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] public bool ShowContainedStateIndicator { get; set; } @@ -85,6 +90,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { + slotIcons = new Sprite[capacity]; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -104,6 +110,17 @@ namespace Barotrauma.Items.Components case "containedstateindicatorempty": ContainedStateIndicatorEmpty = new Sprite(subElement); break; + case "sloticon": + int index = subElement.GetAttributeInt("slotindex", -1); + Sprite icon = new Sprite(subElement); + for (int i = 0; i < capacity; i++) + { + if (i == index || index == -1) + { + slotIcons[i] = icon; + } + } + break; } } @@ -197,7 +214,7 @@ namespace Barotrauma.Items.Components if (UILabel == string.Empty) { return string.Empty; } if (UILabel != null) { - return TextManager.Get("UILabel." + UILabel); + return TextManager.Get("UILabel." + UILabel, returnNull: true) ?? TextManager.Get(UILabel); } else { @@ -205,6 +222,12 @@ namespace Barotrauma.Items.Components } } + public Sprite GetSlotIcon(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= slotIcons.Length) { return null; } + return slotIcons[slotIndex]; + } + public bool KeepOpenWhenEquippedBy(Character character) { if (!character.CanAccessInventory(Inventory) || @@ -266,7 +289,7 @@ namespace Barotrauma.Items.Components } else { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); if (item.body.Dir == -1.0f) { transformedItemPos.X = -transformedItemPos.X; @@ -277,7 +300,7 @@ namespace Barotrauma.Items.Components transformedItemPos = Vector2.Transform(transformedItemPos, transform); transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemPos += item.DrawPosition; + transformedItemPos += item.body.DrawPosition; } Vector2 currentItemPos = transformedItemPos; @@ -319,7 +342,7 @@ namespace Barotrauma.Items.Components new Vector2(currentItemPos.X, -currentItemPos.Y), isWiringMode ? containedItem.GetSpriteColor() * 0.15f : containedItem.GetSpriteColor(), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation + MathHelper.ToRadians(-item.Rotation)), + -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation ), containedItem.Scale, spriteEffects, depth: containedSpriteDepth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index f58aee17b..bff562d65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) + if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn && Light.Enabled) { Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 711376751..6e6fbe63e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -14,12 +14,22 @@ namespace Barotrauma.Items.Components } private GUIButton activateButton; private GUIComponent inputInventoryHolder, outputInventoryHolder; - private GUICustomComponent inputInventoryOverlay; private GUIComponent inSufficientPowerWarning; private bool pendingState; + private GUITextBlock infoArea; + + [Serialize("DeconstructorDeconstruct", true)] + public string ActivateButtonText { get; set; } + + [Serialize("", true)] + public string InfoText { get; set; } + + [Serialize(0.0f, true)] + public float InfoAreaWidth { get; set; } + partial void InitProjSpecific(XElement element) { CreateGUI(); @@ -39,6 +49,12 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + { + TextAlignment = Alignment.Center, + AutoScaleHorizontal = true + }; + var topFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedFrame.RectTransform), style: null); // === INPUT LABEL === // @@ -55,22 +71,23 @@ namespace Barotrauma.Items.Components // === INPUT SLOTS === // inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); - inputInventoryOverlay = new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawOverLay, null) { CanBeFocused = false }; + new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawOverLay, null) { CanBeFocused = false }; // === ACTIVATE BUTTON === // - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.7f), inputArea.RectTransform), childAnchor: Anchor.CenterLeft); + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterLeft); activateButton = new GUIButton(new RectTransform(new Vector2(0.95f, 0.8f), buttonContainer.RectTransform), TextManager.Get("DeconstructorDeconstruct"), style: "DeviceButton") { TextBlock = { AutoScaleHorizontal = true }, OnClicked = ToggleActive }; inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow") + TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) { HoverColor = Color.Black, IgnoreLayoutGroups = true, Visible = false, - CanBeFocused = false + CanBeFocused = false, + AutoScaleHorizontal = true }; // === OUTPUT AREA === // @@ -86,8 +103,70 @@ namespace Barotrauma.Items.Components outputLabel.RectTransform.Resize(new Point((int) outputLabel.Font.MeasureString(outputLabel.Text).X, outputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, outputLabelArea.RectTransform), style: "HorizontalLine"); - // === OUTPUT SLOTS === // - outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), style: null); + var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; + + // === OUTPUT SLOTS === // + outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null); + + if (InfoAreaWidth >= 0.0f) + { + var infoAreaContainer = new GUILayoutGroup(new RectTransform(new Vector2(InfoAreaWidth, 0.8f), outputArea.RectTransform), childAnchor: Anchor.CenterLeft); + infoArea = new GUITextBlock(new RectTransform(new Vector2(0.95f, 0.95f), infoAreaContainer.RectTransform), string.Empty, wrap: true); + } + + ActivateButton.OnAddedToGUIUpdateList += (GUIComponent component) => + { + activateButton.Enabled = true; + if (string.IsNullOrEmpty(InfoText)) + { + infoArea.Text = string.Empty; + } + else + { + infoArea.Text = TextManager.Get(InfoText, returnNull: true) ?? InfoText; + } + if (IsActive) + { + activateButton.Text = TextManager.Get("DeconstructorCancel"); + infoArea.Text = string.Empty; + return; + } + bool outputsFound = false; + foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: true)) + { + outputsFound = true; + if (!string.IsNullOrEmpty(deconstructItem.ActivateButtonText)) + { + string buttonText = TextManager.Get(deconstructItem.ActivateButtonText, returnNull: true) ?? deconstructItem.ActivateButtonText; + string infoText = string.Empty; + if (!string.IsNullOrEmpty(deconstructItem.InfoText)) + { + infoText = TextManager.Get(deconstructItem.InfoText, returnNull: true) ?? deconstructItem.InfoText; + } + inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText); + activateButton.Text = buttonText; + if (infoArea != null) + { + infoArea.Text = infoText; + } + return; + } + } + //no valid outputs found: check if we're missing some required items from the input slots and display a message about it if possible + if (!outputsFound && infoArea != null) + { + foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: false)) + { + if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing)) + { + string missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First(), returnNull: true); + infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); + } + } + } + activateButton.Enabled = outputsFound; + activateButton.Text = TextManager.Get(ActivateButtonText); + }; } public override bool Select(Character character) @@ -126,13 +205,30 @@ namespace Barotrauma.Items.Components private void DrawOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); - var lastSlot = inputContainer.Inventory.visualSlots.Last(); - GUI.DrawRectangle(spriteBatch, - new Rectangle( - lastSlot.Rect.X, lastSlot.Rect.Y + (int)(lastSlot.Rect.Height * (1.0f - progressState)), - lastSlot.Rect.Width, (int)(lastSlot.Rect.Height * progressState)), - GUI.Style.Green * 0.5f, isFilled: true); + if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } + + if (DeconstructItemsSimultaneously) + { + for (int i = 0; i < InputContainer.Inventory.Capacity; i++) + { + if (InputContainer.Inventory.GetItemAt(i) == null) { continue; } + DrawProgressBar(InputContainer.Inventory.visualSlots[i]); + } + } + else + { + DrawProgressBar(inputContainer.Inventory.visualSlots.Last()); + } + + void DrawProgressBar(VisualSlot slot) + { + GUI.DrawRectangle(spriteBatch, + new Rectangle( + slot.Rect.X, slot.Rect.Y + (int)(slot.Rect.Height * (1.0f - progressState)), + slot.Rect.Width, (int)(slot.Rect.Height * progressState)), + GUI.Style.Green * 0.5f, isFilled: true); + } } public override void UpdateHUD(Character character, float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index e63a0fb84..53c770df7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components private FabricationRecipe pendingFabricatedItem; - private Pair tooltip; + private (Rectangle area, string text)? tooltip; private GUITextBlock requiredTimeBlock; @@ -255,19 +255,22 @@ namespace Barotrauma.Items.Components var item1 = c1.GUIComponent.UserData as FabricationRecipe; var item2 = c2.GUIComponent.UserData as FabricationRecipe; - bool hasSkills1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f; - bool hasSkills2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f; + int itemPlacement1 = FabricationDegreeOfSuccess(character, item1.RequiredSkills) >= 0.5f ? 0 : -1; + int itemPlacement2 = FabricationDegreeOfSuccess(character, item2.RequiredSkills) >= 0.5f ? 0 : -1; - if (hasSkills1 != hasSkills2) + itemPlacement1 += item1.RequiresRecipe && !character.HasRecipeForItem(item1.TargetItem.Identifier) ? -2 : 0; + itemPlacement2 += item2.RequiresRecipe && !character.HasRecipeForItem(item2.TargetItem.Identifier) ? -2 : 0; + + if (itemPlacement1 != itemPlacement2) { - return hasSkills1 ? -1 : 1; + return itemPlacement1 > itemPlacement2 ? -1 : 1; } return string.Compare(item1.DisplayName, item2.DisplayName); }); var sufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorsufficientskills", returnNull: true) ?? "Sufficient skills to fabricate", textColor: GUI.Style.Green, font: GUI.SubHeadingFont) + TextManager.Get("fabricatorsufficientskills"), textColor: GUI.Style.Green, font: GUI.SubHeadingFont) { AutoScaleHorizontal = true, CanBeFocused = false @@ -275,7 +278,7 @@ namespace Barotrauma.Items.Components sufficientSkillsText.RectTransform.SetAsFirstChild(); var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorinsufficientskills", returnNull: true) ?? "Insufficient skills to fabricate", textColor: Color.Orange, font: GUI.SubHeadingFont) + TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUI.SubHeadingFont) { AutoScaleHorizontal = true, CanBeFocused = false @@ -285,6 +288,18 @@ namespace Barotrauma.Items.Components { insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform)); } + + var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), + TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUI.SubHeadingFont) + { + AutoScaleHorizontal = true, + CanBeFocused = false + }; + var firstRequiresRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe fabricableItem && (fabricableItem.RequiresRecipe && !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))); + if (firstRequiresRecipe != null) + { + requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstRequiresRecipe.RectTransform)); + } } private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) @@ -297,6 +312,7 @@ namespace Barotrauma.Items.Components int slotIndex = 0; var missingItems = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in targetItem.RequiredItems) { for (int i = 0; i < requiredItem.Amount; i++) @@ -308,6 +324,8 @@ namespace Barotrauma.Items.Components { missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); } + var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); + missingItems = missingItems.Distinct().ToList(); var availableIngredients = GetAvailableIngredients(); @@ -318,30 +336,30 @@ namespace Barotrauma.Items.Components slotIndex++; } - //highlight suitable ingredients in linked inventories - foreach (Item item in availableIngredients) - { - if (item.ParentInventory != inputContainer.Inventory && IsItemValidIngredient(item, requiredItem)) - { - int availableSlotIndex = item.ParentInventory.FindIndex(item); - //slots are null if the inventory has never been displayed - //(linked item, but the UI is not set to be displayed at the same time) - if (item.ParentInventory.visualSlots != null) - { - if (item.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) - { - item.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); - if (slotIndex < inputContainer.Capacity) + requiredItem.ItemPrefabs + .Where(requiredPrefab => availableIngredients.ContainsKey(requiredPrefab.Identifier)) + .ForEach(requiredPrefab => { + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + + availablePrefabs + .Where(availablePrefab => availablePrefab.ParentInventory != inputContainer.Inventory) + .Where(availablePrefab => availablePrefab.ParentInventory.visualSlots != null) //slots are null if the inventory has never been displayed + .ForEach(availablePrefab => { //(linked item, but the UI is not set to be displayed at the same time) + int availableSlotIndex = availablePrefab.ParentInventory.FindIndex(availablePrefab); + + if (availablePrefab.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + availablePrefab.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + if (slotIndex < inputContainer.Capacity) + { + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + } } - } - } - } - } + }); + }); if (slotIndex >= inputContainer.Capacity) { break; } - + var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( @@ -350,11 +368,32 @@ namespace Barotrauma.Items.Components color: requiredItem.ItemPrefabs.First().InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); + + if (missingCounts[requiredItem] > 1) + { + Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); + string stackCountText = "x" + missingCounts[requiredItem]; + stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + } + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { - GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X, slotRect.Bottom - 8, slotRect.Width, 8), Color.Black * 0.8f, true); + DrawConditionBar(spriteBatch, requiredItem.MinCondition); + } + else if (requiredItem.MaxCondition < 1.0f) + { + DrawConditionBar(spriteBatch, requiredItem.MaxCondition); + } + + void DrawConditionBar(SpriteBatch sb, float condition) + { + int spacing = GUI.IntScale(4); + int height = GUI.IntScale(10); + GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, slotRect.Width - spacing * 2, height), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, - new Rectangle(slotRect.X, slotRect.Bottom - 8, (int)(slotRect.Width * requiredItem.MinCondition), 8), + new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, (int)((slotRect.Width - spacing * 2) * condition), height), GUI.Style.Green * 0.8f, true); } @@ -367,6 +406,10 @@ namespace Barotrauma.Items.Components { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } + else if(requiredItem.MaxCondition < 1.0f) + { + toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } else if (requiredItem.MaxCondition <= 0.0f) { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); @@ -375,7 +418,7 @@ namespace Barotrauma.Items.Components { toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; } - tooltip = new Pair(slotRect, toolTipText); + tooltip = (slotRect, toolTipText); } slotIndex++; @@ -415,7 +458,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Second, tooltip.First); + GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.text, tooltip.Value.area); tooltip = null; } } @@ -435,6 +478,22 @@ namespace Barotrauma.Items.Components if (recipe?.DisplayName == null) { continue; } child.Visible = recipe.DisplayName.ToLower().Contains(filter); } + + //go through the elements backwards, and disable the labels ("insufficient skills to fabricate", "recipe required...") if there's no items below them + bool recipeVisible = false; + foreach (GUIComponent child in itemList.Content.Children.Reverse()) + { + if (!(child.UserData is FabricationRecipe recipe)) + { + child.Visible = recipeVisible; + recipeVisible = false; + } + else + { + recipeVisible = child.Visible; + } + } + itemList.UpdateScrollBarSize(); itemList.BarScroll = 0.0f; @@ -470,14 +529,30 @@ namespace Barotrauma.Items.Components }; }*/ + string itemName = GetRecipeNameAndAmount(selectedItem); + string name = itemName; + + float quality = GetFabricatedItemQuality(selectedItem, user); + if (quality > 0) + { + name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n', fallBackTag: "itemname.quality3"); + } var nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - GetRecipeNameAndAmount(selectedItem), textAlignment: Alignment.CenterLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont) + name, textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont, parseRichText: true) { AutoScaleHorizontal = true }; - - nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, nameBlock.Padding.Z, nameBlock.Padding.W); + 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, fallBackTag: "itemname.quality3")); + nameBlock.AutoScaleHorizontal = false; + nameBlock.TextScale = 0.7f; + nameBlock.Wrap = true; + nameBlock.SetTextPos(); + nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale)); + } if (!string.IsNullOrWhiteSpace(selectedItem.TargetItem.Description)) { @@ -489,6 +564,7 @@ namespace Barotrauma.Items.Components while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height) { var lines = description.WrappedText.Split('\n'); + if (lines.Length <= 1) { break; } var newString = string.Join('\n', lines.Take(lines.Length - 1)); description.Text = newString.Substring(0, newString.Length - 4) + "..."; description.CalculateHeightFromText(); @@ -598,10 +674,15 @@ namespace Barotrauma.Items.Components { foreach (GUIComponent child in itemList.Content.Children) { - var itemPrefab = child.UserData as FabricationRecipe; - if (itemPrefab == null) continue; + if (!(child.UserData is FabricationRecipe itemPrefab)) { continue; } - bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients); + if (itemPrefab != selectedItem && + (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y)) + { + continue; + } + + bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients, character); if (itemPrefab == selectedItem) { activateButton.Enabled = canBeFabricated; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 0ec56396d..890b2910a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1,103 +1,573 @@ -using Barotrauma.Extensions; -using FarseerPhysics; +#nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; +using Microsoft.Xna.Framework.Input; namespace Barotrauma.Items.Components { + internal readonly struct MiniMapGUIComponent + { + public readonly GUIComponent RectComponent; + public readonly GUIComponent BorderComponent; + + public MiniMapGUIComponent(GUIComponent rectComponent) + { + RectComponent = rectComponent; + BorderComponent = rectComponent; + } + + public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent) + { + RectComponent = frame; + BorderComponent = linkedHullComponent; + } + + public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent) + { + component = RectComponent; + borderComponent = BorderComponent; + } + } + + internal readonly struct MiniMapSprite + { + public readonly Sprite Sprite; + public readonly Color Color; + + public MiniMapSprite(JobPrefab prefab) + { + Sprite = prefab.IconSmall; + Color = prefab.UIColor; + } + + public MiniMapSprite(Order order) + { + Sprite = order.SymbolSprite; + Color = order.Color; + } + } + + internal readonly struct MiniMapHullData + { + public readonly List> Polygon; + public readonly (RectangleF Rect, Hull Hull)[] RectDatas; + public readonly RectangleF Bounds; + public readonly Point ParentSize; + + public MiniMapHullData(List> polygon, RectangleF bounds, Point parentSize, ImmutableArray rects, ImmutableArray hulls) + { + ParentSize = parentSize; + Bounds = bounds; + Polygon = polygon; + int count = Math.Min(rects.Length, hulls.Length); + RectDatas = new (RectangleF Rect, Hull Hull)[count]; + for (int i = 0; i < count; i++) + { + RectDatas[i] = (rects[i], hulls[i]); + } + } + } + + internal enum MiniMapMode + { + None, + HullStatus, + ElectricalView, + ItemFinder + } + + internal readonly struct RelativeEntityRect + { + public readonly Vector2 RelativePosition; + public readonly Vector2 RelativeSize; + + public RelativeEntityRect(RectangleF worldBorders, RectangleF entityRect) + { + RelativePosition = new Vector2((entityRect.X - worldBorders.X) / worldBorders.Width, (worldBorders.Y - entityRect.Y) / worldBorders.Height); + RelativeSize = new Vector2(entityRect.Width / worldBorders.Width, entityRect.Height / worldBorders.Height); + } + + public Vector2 PositionRelativeTo(RectangleF frame, bool skipOffset = false) + { + if (skipOffset) + { + return RelativePosition * frame.Size; + } + + return frame.Location + RelativePosition * frame.Size; + } + + public Vector2 SizeRelativeTo(RectangleF frame) + { + return RelativeSize * frame.Size; + } + + public RectangleF RectangleRelativeTo(RectangleF frame, bool skipOffset = false) + { + return new RectangleF(PositionRelativeTo(frame, skipOffset), SizeRelativeTo(frame)); + } + + public void Deconstruct(out float posX, out float posY, out float sizeX, out float sizeY) + { + posX = RelativePosition.X; + posY = RelativePosition.Y; + sizeX = RelativeSize.X; + sizeY = RelativeSize.Y; + } + } + + internal readonly struct MiniMapSettings + { + public static MiniMapSettings Default = new MiniMapSettings + ( + ignoreOutposts: false, + createHullElements: true, + elementColor: MiniMap.MiniMapBaseColor + ); + + public readonly bool IgnoreOutposts; + public readonly bool CreateHullElements; + public readonly Color ElementColor; + + public MiniMapSettings(bool ignoreOutposts = false, bool createHullElements = false, Color? elementColor = null) + { + IgnoreOutposts = ignoreOutposts; + CreateHullElements = createHullElements; + ElementColor = elementColor ?? MiniMap.MiniMapBaseColor; + } + } + partial class MiniMap : Powered { private GUIFrame submarineContainer; private GUIFrame hullInfoFrame; + private GUIScissorComponent? scissorComponent; + private GUIComponent? miniMapContainer; + private GUIComponent miniMapFrame; + private GUIComponent electricalFrame; + private GUILayoutGroup reportFrame; + private GUILayoutGroup searchBarFrame; + private GUITextBox searchBar; + private GUIComponent? searchAutoComplete; - private GUITextBlock hullNameText, hullBreachText, hullAirQualityText, hullWaterText; + private ItemPrefab? searchedPrefab; - private string noPowerTip = ""; + private GUITextBlock tooltipHeader, tooltipFirstLine, tooltipSecondLine, tooltipThirdLine; + + private string noPowerTip = string.Empty; private readonly List displayedSubs = new List(); private Point prevResolution; + private float cardRefreshTimer; + private const float cardRefreshDelay = 3f; - partial void InitProjSpecific(XElement element) + private readonly HashSet cardsToDraw = new HashSet(); + + private List subEntities = new List(); + + private Texture2D? submarinePreview; + + private MiniMapMode currentMode; + private ImmutableArray modeSwitchButtons; + + private Point elementSize; + + private ImmutableDictionary hullStatusComponents; + private ImmutableDictionary electricalMapComponents; + private ImmutableDictionary electricalChildren; + private ImmutableDictionary doorChildren; + + private ImmutableHashSet? itemsFoundOnSub; + + private ImmutableHashSet? MiniMapBlips; + private float blipState; + private const float maxBlipState = 1f; + + private const float maxZoom = 10f, + minZoom = 0.5f, + defaultZoom = 1f; + + private float zoom = defaultZoom; + + private float Zoom { + get => zoom; + set => zoom = Math.Clamp(value, minZoom, maxZoom); + } + + private Vector2 mapOffset = Vector2.Zero; + private bool dragMap; + private Vector2? dragMapStart; + private const int dragTreshold = 8; + + private bool recalculate; + + public static readonly Color MiniMapBaseColor = new Color(15, 178, 107); + + private static readonly Color WetHullColor = new Color(11, 122, 205), + DoorIndicatorColor = GUI.Style.Green, + NoPowerDoorColor = DoorIndicatorColor * 0.1f, + DefaultNeutralColor = MiniMapBaseColor * 0.8f, + HoverColor = Color.White, + BlueprintBlue = new Color(23, 38, 33), + HullWaterColor = new Color(17, 173, 179) * 0.5f, + HullWaterLineColor = Color.LightBlue * 0.5f, + NoPowerColor = MiniMapBaseColor * 0.1f, + ElectricalBaseColor = GUI.Style.Orange, + NoPowerElectricalColor = ElectricalBaseColor * 0.1f; + + partial void InitProjSpecific() + { + SetDefaultMode(); + noPowerTip = TextManager.Get("SteeringNoPowerTip"); CreateGUI(); } + private void SetDefaultMode() + { + currentMode = true switch + { + true when EnableHullStatus => MiniMapMode.HullStatus, + true when EnableElectricalView => MiniMapMode.ElectricalView, + true when EnableItemFinder => MiniMapMode.ItemFinder, + _ => MiniMapMode.None + }; + } + protected override void CreateGUI() { + GuiFrame.ClearChildren(); + GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); GuiFrame.CanBeFocused = true; - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, - DrawHUDBack, null); - submarineContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), style: null); - - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, - DrawHUDFront, null) + var submarineBack = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDBack, null); + GUIFrame paddedContainer = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center), style: null); + submarineContainer = new GUIFrame(new RectTransform(Vector2.One, paddedContainer.RectTransform, Anchor.Center), style: null); + var submarineFront = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDFront, null) { CanBeFocused = false }; - hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), - style: "GUIToolTip") + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, isHorizontal: true) { CanBeFocused = true }; + + modeSwitchButtons = ImmutableArray.Create + ( + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullStatus") { UserData = MiniMapMode.HullStatus, Enabled = EnableHullStatus, ToolTip = TextManager.Get("StatusMonitorButton.HullStatus.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ElectricalView") { UserData = MiniMapMode.ElectricalView, Enabled = EnableElectricalView, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ItemFinder") { UserData = MiniMapMode.ItemFinder, Enabled = EnableItemFinder, ToolTip = TextManager.Get("StatusMonitorButton.ItemFinder.Tooltip") } + ); + + foreach (GUIButton button in modeSwitchButtons) + { + button.OnClicked = (btn, o) => + { + if (!(o is MiniMapMode m)) { return false; } + + currentMode = m; + Zoom = defaultZoom; + mapOffset = Vector2.Zero; + recalculate = true; + + foreach (GUIButton otherButton in modeSwitchButtons) + { + otherButton.Selected = false; + } + + btn.Selected = true; + return true; + }; + + if (button.UserData is MiniMapMode buttonMode) + { + button.Selected = currentMode == buttonMode; + } + } + + List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + + GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { CanBeFocused = false }; + + reportFrame = new GUILayoutGroup(new RectTransform(new Vector2(1), bottomFrame.RectTransform), isHorizontal: true) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) + }; + + if (reports.Any()) + { + CrewManager.CreateReportButtons(GameMain.GameSession?.CrewManager, reportFrame, reports, true); + } + + searchBarFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.5f, 1.0f), bottomFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.Center) + { + Visible = false + }; + searchBar = new GUITextBox(new RectTransform(new Vector2(1), searchBarFrame.RectTransform), string.Empty, createClearButton: true, createPenIcon: true) + { + OnEnterPressed = (box, text) => + { + SearchItems(text); + return true; + } + }; + + searchAutoComplete = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIToolTip") + { + Visible = false, + CanBeFocused = false + }; + + SetAutoCompletePosition(searchAutoComplete, searchBar); + + GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform)) + { + OnSelected = (component, o) => + { + if (o is ItemPrefab prefab) + { + searchedPrefab = prefab; + searchBar.TextBlock.Text = prefab.Name; + searchBar.Deselect(); + SearchItems(searchBar.Text); + } + return true; + } + }; + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(prefab => prefab.Name)) + { + if (prefab.HideInMenus) { continue; } + CreateItemFrame(prefab, listBox.Content.RectTransform); + } + + searchBar.OnDeselected += (sender, key) => + { + searchAutoComplete.Visible = false; + }; + + searchBar.OnSelected += (sender, key) => + { + itemsFoundOnSub = Item.ItemList.Where(it => VisibleOnItemFinder(it)).Select(it => it.Prefab).ToImmutableHashSet(); + }; + + searchBar.OnKeyHit += ControlSearchTooltip; + searchBar.OnTextChanged += UpdateSearchTooltip; + + hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip") + { + CanBeFocused = false + + }; + var hullInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), hullInfoFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.05f }; - hullNameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullBreachText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullAirQualityText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; - hullWaterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), "") { Wrap = true }; + tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipFirstLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipSecondLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; + tooltipThirdLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true }; hullInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); + + submarineBack.RectTransform.MaxSize = + submarineFront.RectTransform.MaxSize = + submarineContainer.RectTransform.MaxSize = + new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height); } - public override void AddToGUIUpdateList() + private bool VisibleOnItemFinder(Item it) { - base.AddToGUIUpdateList(); - hullInfoFrame.AddToGUIUpdateList(order: 1); + if (it.Submarine != item.Submarine) { return false; } + if (it.NonInteractable || it.HiddenInGame) { return false; } + if (it.GetComponent() == null) { return false; } + + var holdable = it.GetComponent(); + if (holdable != null && holdable.Attached) { return false; } + + var wire = it.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + + if (it.HasTag("traitormissionitem")) { return false; } + + return true; + } + + public override void AddToGUIUpdateList(int order = 0) + { + base.AddToGUIUpdateList(order); + hullInfoFrame.AddToGUIUpdateList(order: order + 1); + if (currentMode == MiniMapMode.ItemFinder && searchBar.Selected) + { + searchAutoComplete?.AddToGUIUpdateList(order: order + 1); + } } private void CreateHUD() { + subEntities.Clear(); prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - submarineContainer?.ClearChildren(); + submarineContainer.ClearChildren(); - if (item.Submarine == null) { return; } + if (item.Submarine is null) { return; } + + scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); + miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; + + ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && it.GetComponent() != null).ToImmutableHashSet(); + miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents); + + IEnumerable electrialPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); + electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents); + + Dictionary electricChildren = new Dictionary(); + + foreach (var (entity, component) in electricalMapComponents) + { + GUIComponent parent = component.RectComponent; + if (!(entity is Item it )) { continue; } + Sprite? sprite = it.Prefab.UpgradePreviewSprite; + if (sprite is null) { continue; } + + GUIImage child = new GUIImage(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), sprite) + { + OutlineColor = ElectricalBaseColor, + Color = ElectricalBaseColor, + HoverCursor = CursorState.Hand, + SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None + }; + + electricChildren.Add(component, child); + } + + electricalChildren = electricChildren.ToImmutableDictionary(); + + Dictionary doorChilds = new Dictionary(); + + foreach (var (entity, component) in hullStatusComponents) + { + if (!hullPointsOfInterest.Contains(entity)) { continue; } + + const int minSize = 8; + const int borderMaxSize = 2; + + Point size = component.BorderComponent.Rect.Size; + + size.X = Math.Max(size.X, minSize); + size.Y = Math.Max(size.Y, minSize); + float width = Math.Min(borderMaxSize, Math.Min(size.X, size.Y) / 8f); + + GUIFrame frame = new GUIFrame(new RectTransform(size, component.RectComponent.RectTransform, anchor: Anchor.Center), style: "ScanLines", color: DoorIndicatorColor) + { + OutlineColor = GUI.Style.Green, + OutlineThickness = width + }; + doorChilds.Add(component, frame); + } + + doorChildren = doorChilds.ToImmutableDictionary(); + + Rectangle parentRect = miniMapFrame.Rect; - item.Submarine.CreateMiniMap(submarineContainer); displayedSubs.Clear(); displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo); + + subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + + BakeSubmarine(item.Submarine, parentRect); + elementSize = GuiFrame.Rect.Size; } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed - if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs - !displayedSubs.Contains(item.Submarine) || //current sub not displayer - prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || //resolution changed - item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed - !submarineContainer.Children.Any() || // We lack a GUI - displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed + if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs + item.Submarine is { } itemSub && + ( + !displayedSubs.Contains(itemSub) || // current sub not displayed + itemSub.DockedTo.Any(s => !displayedSubs.Contains(s)) || // some of the docked subs not displayed + displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed + ) || + prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed + !submarineContainer.Children.Any()) // We lack a GUI { CreateHUD(); } - + + if (scissorComponent != null) + { + if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus) + { + if (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn)) + { + dragMapStart = PlayerInput.MousePosition; + } + } + + if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) + { + float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); + float distanceScale = newZoom / Zoom; + mapOffset *= distanceScale; + recalculate |= !MathUtils.NearlyEqual(Zoom, newZoom); + Zoom = newZoom; + } + } + + if (dragMapStart is { } dragStart) + { + if (dragMap || Vector2.DistanceSquared(dragStart, PlayerInput.MousePosition) > GUI.IntScale(dragTreshold * dragTreshold)) + { + mapOffset.X += PlayerInput.MouseSpeed.X; + mapOffset.Y += PlayerInput.MouseSpeed.Y; + + recalculate = true; + dragMap = true; + } + } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + dragMapStart = null; + dragMap = false; + } + + if (recalculate) + { + if (miniMapContainer != null) + { + miniMapContainer.RectTransform.LocalScale = new Vector2(Zoom); + miniMapContainer.RectTransform.RecalculateChildren(true, true); + miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint(); + } + recalculate = false; + } + + // is there a better way to do this? + if (GuiFrame.Rect.Size != elementSize) + { + CreateGUI(); + elementSize = GuiFrame.Rect.Size; + } + float distort = 1.0f - item.Condition / item.MaxCondition; foreach (HullData hullData in hullDatas.Values) { @@ -107,12 +577,52 @@ namespace Barotrauma.Items.Components hullData.Distort = Rand.Range(0.0f, 1.0f) < distort * distort; if (hullData.Distort) { - hullData.Oxygen = Rand.Range(0.0f, 100.0f); - hullData.Water = Rand.Range(0.0f, 1.0f); + hullData.ReceivedOxygenAmount = Rand.Range(0.0f, 100.0f); + hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); } hullData.DistortionTimer = Rand.Range(1.0f, 10.0f); } } + + UpdateHUDBack(); + + if (blipState > maxBlipState) + { + blipState = 0; + } + + blipState += deltaTime; + + if (currentMode == MiniMapMode.HullStatus && !EnableHullStatus || + currentMode == MiniMapMode.ElectricalView && !EnableElectricalView || + currentMode == MiniMapMode.ItemFinder && !EnableItemFinder) + { + SetDefaultMode(); + } + + modeSwitchButtons[0].Enabled = EnableHullStatus; + modeSwitchButtons[1].Enabled = EnableElectricalView; + modeSwitchButtons[2].Enabled = EnableItemFinder; + } + + private void UpdateIDCards(Submarine sub) + { + if (hullDatas is null) { return; } + + foreach (HullData data in hullDatas.Values) + { + data.Cards.Clear(); + } + + foreach (Item it in sub.GetItems(true)) + { + if (it is { CurrentHull: { } hull } && it.GetComponent() is { } idCard && idCard.TeamID == sub.TeamID) + { + if (!hullDatas.ContainsKey(hull)) { continue; } + + hullDatas[hull].Cards.Add(idCard); + } + } } private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) @@ -121,209 +631,728 @@ namespace Barotrauma.Items.Components { Vector2 textSize = GUI.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); + Color noPowerColor = GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); - GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); - return; - } - if (!submarineContainer.Children.Any()) { return; } - foreach (GUIComponent child in submarineContainer.Children.FirstOrDefault()?.Children) - { - if (child.UserData is Hull hull) - { - if (hull.Submarine == null || !hull.Submarine.Info.IsOutpost) { continue; } - string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Info.Name); - Vector2 textSize = GUI.Font.MeasureString(text); - Vector2 textPos = child.Center; - if (textPos.X + textSize.X / 2 > submarineContainer.Rect.Right) - textPos.X -= ((textPos.X + textSize.X / 2) - submarineContainer.Rect.Right) + 10 * GUI.xScale; - if (textPos.X - textSize.X / 2 < submarineContainer.Rect.X) - textPos.X += (submarineContainer.Rect.X - (textPos.X - textSize.X / 2)) + 10 * GUI.xScale; - GUI.DrawString(spriteBatch, textPos - textSize / 2, text, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f); - break; - } - } - } - - private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) - { - Hull mouseOnHull = null; - hullInfoFrame.Visible = false; - - foreach (Hull hull in Hull.hullList) - { - var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); - if (hullFrame == null) { continue; } - - if (GUI.MouseOn == hullFrame || hullFrame.IsParentOf(GUI.MouseOn)) - { - mouseOnHull = hull; - } - if (item.Submarine == null || !hasPower) - { - hullFrame.Color = Color.DarkCyan * 0.3f; - hullFrame.Children.First().Color = Color.DarkCyan * 0.3f; - } - } - - if (Voltage < MinVoltage) - { + GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUI.SubHeadingFont); return; } - float scale = 1.0f; - HashSet subs = new HashSet(); - foreach (Hull hull in Hull.hullList) + if (currentMode == MiniMapMode.HullStatus) { - if (hull.Submarine == null) { continue; } - var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); - if (hullFrame == null) { continue; } + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; - hullFrame.Visible = true; - if (!submarineContainer.Rect.Contains(hullFrame.Rect)) + if (item.Submarine != null) { - if (hull.Submarine.Info.Type != SubmarineType.Player) + var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; + float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; + if (sprite != null) { - hullFrame.Visible = false; - continue; + Vector2 spriteSize = sprite.size; + Rectangle worldBorders = item.Submarine.GetDockedBorders(); + worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); + foreach (Gap gap in Gap.GapList) + { + if (gap.IsRoomToRoom || gap.Submarine != item.Submarine || gap.ConnectedDoor != null) { continue; } + RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders); + + Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; + + Color color = ToolBox.GradientLerp(gap.Open, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorLow) * alpha; + sprite.Draw(spriteBatch, + miniMapFrame.Rect.Location.ToVector2() + entityRect.Center, + color, origin: sprite.Origin, rotate: 0.0f, scale: scale); + } } } - hullDatas.TryGetValue(hull, out HullData hullData); - if (hullData == null) + if (currentMode == MiniMapMode.HullStatus) + { + foreach (var (entity, component) in hullStatusComponents) + { + if (!(entity is Hull hull)) { continue; } + if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } + DrawHullCards(spriteBatch, hull, hullData, component.RectComponent); + } + } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } + } + + private void ControlSearchTooltip(GUITextBox sender, Keys key) + { + if (searchAutoComplete is null || !searchAutoComplete.Visible) { return; } + GUIListBox listBox = searchAutoComplete.GetChild(); + if (listBox is null) { return; } + + if (key == Keys.Down) + { + listBox.SelectNext(true, autoScroll: true); + } + else if (key == Keys.Up) + { + listBox.SelectPrevious(true, autoScroll: true); + } + else if (key == Keys.Enter) + { + listBox.OnSelected?.Invoke(listBox, listBox.SelectedData); + searchBar.Deselect(); + } + } + + private bool UpdateSearchTooltip(GUITextBox box, string? text) + { + if (text is null || itemsFoundOnSub is null || searchAutoComplete is null) { return false; } + + MiniMapBlips = null; + searchedPrefab = null; + searchAutoComplete.Visible = true; + SetAutoCompletePosition(searchAutoComplete, box); + + GUIListBox? listBox = searchAutoComplete.GetChild(); + if (listBox?.Content is null) { return false; } + + bool first = true; + + int i = 0; + + foreach (GUIComponent component in listBox.Content.Children) + { + component.Visible = false; + if (component.UserData is ItemPrefab { Name: { } prefabName} prefab && itemsFoundOnSub.Contains(prefab)) + { + component.Visible = prefabName.ToLower().Contains(text.ToLower()); + + if (component.Visible && first) + { + listBox.Select(i, force: true, autoScroll: false); + first = false; + } + } + + i++; + } + + listBox.BarScroll = 0f; + listBox.RecalculateChildren(); + + return true; + } + + private void SetAutoCompletePosition(GUIComponent tooltip, GUITextBox box) + { + int height = GuiFrame.Rect.Height / 2; + tooltip.RectTransform.NonScaledSize = new Point(box.Rect.Width, height); + tooltip.RectTransform.ScreenSpaceOffset = new Point(box.Rect.X, box.Rect.Y - height); + } + + private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) + { + Sprite sprite = prefab.InventoryIcon ?? prefab.sprite; + if (sprite is null) { return; } + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") + { + UserData = prefab + }; + + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUIImage(new RectTransform(Vector2.One, layout.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite) + { + Color = prefab.InventoryIconColor, + UserData = prefab + }; + + var nameText = new GUITextBlock(new RectTransform(Vector2.One, layout.RectTransform), prefab.Name); + nameText.RectTransform.SizeChanged += () => + { + nameText.Text = ToolBox.LimitString(prefab.Name, nameText.Font, nameText.Rect.Width); + }; + } + + private void SearchItems(string text) + { + if (searchedPrefab is null) + { + ItemPrefab? first = ItemPrefab.Prefabs.FirstOrDefault(p => p.Name.ToLower().Equals(text.ToLower())); + + if (first is null) + { + searchBar.Flash(GUI.Style.Red); + return; + } + searchedPrefab = first; + } + + if (item.Submarine is null) { return; } + + HashSet foundItems = new HashSet(); + + foreach (Item it in Item.ItemList) + { + if (!VisibleOnItemFinder(it)) { continue; } + + if (it.Prefab == searchedPrefab) + { + // ignore items on players and hidden inventories + if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { HiddenInGame: true }}) is { }) { continue; } + + if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent) + { + foundItems.Add((Item)parent.Owner); + } + else + { + foundItems.Add(it); + } + } + } + + + RectangleF dockedBorders = item.Submarine.GetDockedBorders(); + dockedBorders.Location += item.Submarine.WorldPosition; + RectangleF parentRect = miniMapFrame.Rect; + + HashSet positions = new HashSet(); + foreach (Item foundItem in foundItems) + { + RelativeEntityRect scaledRect = new RelativeEntityRect(dockedBorders, foundItem.WorldRect); + Vector2 pos = scaledRect.PositionRelativeTo(parentRect, skipOffset: true) + scaledRect.SizeRelativeTo(parentRect) / 2f; + positions.Add(pos); + } + + MiniMapBlips = positions.ToImmutableHashSet(); + + if (searchAutoComplete is null) { return; } + searchAutoComplete.Visible = false; + } + + private void UpdateHUDBack() + { + if (item.Submarine == null) { return; } + + hullInfoFrame.Visible = false; + reportFrame.Visible = false; + searchBarFrame.Visible = false; + electricalFrame.Visible = false; + miniMapFrame.Visible = false; + + switch (currentMode) + { + case MiniMapMode.HullStatus: + UpdateHullStatus(); + miniMapFrame.Visible = true; + reportFrame.Visible = true; + break; + case MiniMapMode.ElectricalView: + UpdateElectricalView(); + electricalFrame.Visible = true; + break; + case MiniMapMode.ItemFinder: + searchBarFrame.Visible = true; + break; + } + } + + private void UpdateHullStatus() + { + bool canHoverOverHull = true; + + foreach (var (entity, component) in hullStatusComponents) + { + // we are only interested in non-hull components + if (entity is Hull) { continue; } + + GUIComponent rectComponent = component.RectComponent; + + if (doorChildren.TryGetValue(component, out GUIComponent? child) && child != null) + { + if (item.Submarine == null || !hasPower) + { + child.Color = child.OutlineColor = NoPowerDoorColor; + } + + if (Voltage < MinVoltage) { continue; } + + child.Color = child.OutlineColor = DoorIndicatorColor; + if (GUI.MouseOn == child) + { + SetTooltip(rectComponent.Rect.Center, entity.Name, string.Empty, string.Empty, string.Empty); + canHoverOverHull = false; + child.Color = child.OutlineColor = HoverColor; + } + } + } + + foreach (var (entity, (component, borderComponent)) in hullStatusComponents) + { + if (item.Submarine == null || !hasPower) + { + component.Color = borderComponent.OutlineColor = NoPowerColor; + } + + if (Voltage < MinVoltage) { continue; } + + if (!component.Visible) { continue; } + if (!(entity is Hull hull)) { continue; } + + if (!submarineContainer.Rect.Contains(component.Rect)) + { + if (hull.Submarine.Info.Type != SubmarineType.Player) + { + component.Visible = borderComponent.Visible = false; + continue; + } + } + + hullDatas.TryGetValue(hull, out HullData? hullData); + if (hullData is null) { hullData = new HullData(); GetLinkedHulls(hull, hullData.LinkedHulls); hullDatas.Add(hull, hullData); } - - Color neutralColor = Color.DarkCyan; + + Color neutralColor = DefaultNeutralColor; + Color borderColor = neutralColor; + Color componentColor; + if (hull.IsWetRoom) { - neutralColor = new Color(9, 80, 159); + neutralColor = WetHullColor; } if (hullData.Distort) { - hullFrame.Children.First().Color = Color.Lerp(Color.Black, Color.DarkGray * 0.5f, Rand.Range(0.0f, 1.0f)); - hullFrame.Color = neutralColor * 0.5f; + borderComponent.OutlineColor = neutralColor * 0.5f; + component.Color = Color.Lerp(Color.Black, Color.DarkGray * 0.5f, Rand.Range(0.0f, 1.0f)); continue; } - - subs.Add(hull.Submarine); - scale = Math.Min( - hullFrame.Parent.Rect.Width / (float)hull.Submarine.Borders.Width, - hullFrame.Parent.Rect.Height / (float)hull.Submarine.Borders.Height); - - Color borderColor = neutralColor; - - float? gapOpenSum = 0.0f; + + hullData.HullOxygenAmount = RequireOxygenDetectors ? hullData.ReceivedOxygenAmount : hull.OxygenPercentage; + hullData.HullWaterAmount = RequireWaterDetectors ? hullData.ReceivedWaterAmount : Math.Min(hull.WaterVolume / hull.Volume, 1.0f); + + float gapOpenSum = 0.0f; + if (ShowHullIntegrity) { - gapOpenSum = hull.ConnectedGaps.Where(g => !g.IsRoomToRoom).Sum(g => g.Open); - borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min((float)gapOpenSum, 1.0f)); + float amount = 1f + hullData.LinkedHulls.Count; + gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom).Sum(g => g.Open) / amount; + borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); } - float? oxygenAmount = null; - if (!RequireOxygenDetectors || hullData?.Oxygen != null) + bool isHoveringOver = canHoverOverHull && GUI.MouseOn == component; + + // When drawing tooltip we are only interested in the component we are hovering over + if (isHoveringOver) { - oxygenAmount = RequireOxygenDetectors ? hullData.Oxygen : hull.OxygenPercentage; - GUI.DrawRectangle( - spriteBatch, hullFrame.Rect, - Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, (float)oxygenAmount / 100.0f), - true); - } + string header = hull.DisplayName; - float? waterAmount = null; - if (!RequireWaterDetectors || hullData.Water != null) - { - waterAmount = RequireWaterDetectors ? hullData.Water : Math.Min(hull.WaterVolume / hull.Volume, 1.0f); - if (hullFrame.Rect.Height * waterAmount > 3.0f) - { - Rectangle waterRect = new Rectangle( - hullFrame.Rect.X, (int)(hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount)), - hullFrame.Rect.Width, (int)(hullFrame.Rect.Height * waterAmount)); - - waterRect.Inflate(-3, -3); - - GUI.DrawRectangle(spriteBatch, waterRect, new Color(85, 136, 147), true); - GUI.DrawLine(spriteBatch, new Vector2(waterRect.X, waterRect.Y), new Vector2(waterRect.Right, waterRect.Y), Color.LightBlue); - } - } - - if (mouseOnHull == hull || - hullData.LinkedHulls.Contains(mouseOnHull)) - { - borderColor = Color.Lerp(borderColor, Color.White, 0.5f); - hullFrame.Children.First().Color = Color.White; - hullFrame.Color = borderColor; - } - else - { - hullFrame.Children.First().Color = neutralColor * 0.8f; - } - - if (mouseOnHull == hull) - { - hullInfoFrame.RectTransform.ScreenSpaceOffset = hullFrame.Rect.Center; - if (hullInfoFrame.Rect.Right > GameMain.GraphicsWidth) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(hullInfoFrame.Rect.Width, 0); } - if (hullInfoFrame.Rect.Bottom > GameMain.GraphicsHeight) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(0, hullInfoFrame.Rect.Height); } - - hullInfoFrame.Visible = true; - hullNameText.Text = hull.DisplayName; + float? oxygenAmount = hullData.HullOxygenAmount, + waterAmount = hullData.HullWaterAmount; foreach (Hull linkedHull in hullData.LinkedHulls) { - gapOpenSum += linkedHull.ConnectedGaps.Where(g => !g.IsRoomToRoom).Sum(g => g.Open); oxygenAmount += linkedHull.OxygenPercentage; waterAmount += Math.Min(linkedHull.WaterVolume / linkedHull.Volume, 1.0f); } + oxygenAmount /= (hullData.LinkedHulls.Count + 1); waterAmount /= (hullData.LinkedHulls.Count + 1); - hullBreachText.Text = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : ""; - hullBreachText.TextColor = GUI.Style.Red; + string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; + Color line1Color = GUI.Style.Red; - hullAirQualityText.Text = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), + (int)oxygenAmount + " %"); - hullAirQualityText.TextColor = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); + string line2 = oxygenAmount == null ? + TextManager.Get("MiniMapAirQualityUnavailable") : + TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), (int)Math.Round(oxygenAmount.Value) + "%"); + Color line2Color = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); - hullWaterText.Text = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : - TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)(waterAmount * 100.0f) + " %"); - hullWaterText.TextColor = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + string line3 = waterAmount == null ? + TextManager.Get("MiniMapWaterLevelUnavailable") : + TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); + Color line3Color = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + + SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } - - hullFrame.Color = borderColor; - } - - foreach (Submarine sub in subs) - { - if (sub.HullVertices == null || sub.Info.IsOutpost) { continue; } - - Rectangle worldBorders = sub.GetDockedBorders(); - worldBorders.Location += sub.WorldPosition.ToPoint(); - - scale = Math.Min( - submarineContainer.Rect.Width / (float)worldBorders.Width, - submarineContainer.Rect.Height / (float)worldBorders.Height) * 0.9f; - float displayScale = ConvertUnits.ToDisplayUnits(scale); - Vector2 offset = ConvertUnits.ToSimUnits(sub.WorldPosition - new Vector2(worldBorders.Center.X, worldBorders.Y - worldBorders.Height / 2)); - Vector2 center = container.Rect.Center.ToVector2(); - - for (int i = 0; i < sub.HullVertices.Count; i++) + bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrder != null; + // When setting the colors we want to know the linked hulls too or else the linked hull will not realize its being hovered over and reset the border color + foreach (Hull linkedHull in hullData.LinkedHulls) { - Vector2 start = (sub.HullVertices[i] + offset) * displayScale; - start.Y = -start.Y; - Vector2 end = (sub.HullVertices[(i + 1) % sub.HullVertices.Count] + offset) * displayScale; - end.Y = -end.Y; - GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: (int)(10 * GUI.Scale)); + if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; } + + isHoveringOver |= + canHoverOverHull && + (hullStatusComponents[linkedHull].RectComponent == GUI.MouseOn || (draggingReport && hullStatusComponents[linkedHull].RectComponent.MouseRect.Contains(PlayerInput.MousePosition))); + if (isHoveringOver) { break; } } + + if (isHoveringOver || (draggingReport && component.MouseRect.Contains(PlayerInput.MousePosition))) + { + borderColor = Color.Lerp(borderColor, Color.White, 0.5f); + componentColor = HoverColor; + } + else + { + componentColor = neutralColor * 0.8f; + } + + borderComponent.OutlineColor = borderColor; + component.Color = componentColor; } } - private void GetLinkedHulls(Hull hull, List linkedHulls) + private void UpdateElectricalView() + { + foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) + { + if (!(entity is Item it)) { continue; } + if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + + if (item.Submarine == null || !hasPower) + { + component.Color = component.OutlineColor = NoPowerElectricalColor; + } + + if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; } + + int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f); + Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green); + + if (GUI.MouseOn == component) + { + string line1 = string.Empty; + string line2 = string.Empty; + + if (it.GetComponent() is { } battery) + { + int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f); + line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString()); + } + else if (it.GetComponent() is { } powerTransfer) + { + int current = (int) -powerTransfer.CurrPowerConsumption, + load = (int) powerTransfer.PowerLoad; + + line1 = TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString()); + line2 = TextManager.GetWithVariable("statusmonitor.junctionload.tooltip", "[amount]", load.ToString()); + } + + string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); + SetTooltip(component.Rect.Center, it.Prefab.Name, line1, line2, line3, line3Color: color); + color = HoverColor; + } + + component.Color = component.OutlineColor = color; + } + } + + private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container) + { + if (item.Submarine != null) + { + DrawSubmarine(spriteBatch); + } + + if (Voltage < MinVoltage) { return; } + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; + + if (currentMode == MiniMapMode.ItemFinder) + { + if (MiniMapBlips != null) + { + foreach (Vector2 blip in MiniMapBlips) + { + Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2(); + Sprite pingCircle = GUI.Style.PingCircle.Sprite; + Vector2 targetSize = new Vector2(parentSize.X / 4f); + Vector2 spriteScale = targetSize / pingCircle.size; + float scale = Math.Min(blipState, maxBlipState / 2f); + float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f); + pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); + } + } + } + else + { + bool hullsVisible = currentMode == MiniMapMode.HullStatus; + + foreach (var (entity, component) in hullStatusComponents) + { + if (!(entity is Hull hull)) { continue; } + if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } + + if (hullData.Distort) { continue; } + + GUIComponent hullFrame = component.RectComponent; + + if (hullsVisible && hullData.HullWaterAmount is { } waterAmount) + { + if (hullFrame.Rect.Height * waterAmount > 3.0f) + { + RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); + + const float width = 1f; + + GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor); + + if (!MathUtils.NearlyEqual(waterAmount, 1.0f)) + { + Vector2 offset = new Vector2(0, width); + GUI.DrawLine(spriteBatch, waterRect.Location + offset, new Vector2(waterRect.Right, waterRect.Y) + offset, HullWaterLineColor, width: width); + } + } + } + + if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) + { + GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + } + } + } + + spriteBatch.End(); + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } + + private void SetTooltip(Point pos, string header, string line1, string line2, string line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) + { + hullInfoFrame.RectTransform.ScreenSpaceOffset = pos; + + if (hullInfoFrame.Rect.Left > submarineContainer.Rect.Right) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(submarineContainer.Rect.Right, hullInfoFrame.RectTransform.ScreenSpaceOffset.Y); } + if (hullInfoFrame.Rect.Top > submarineContainer.Rect.Bottom) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(hullInfoFrame.RectTransform.ScreenSpaceOffset.X, submarineContainer.Rect.Bottom); } + + if (hullInfoFrame.Rect.Right > GameMain.GraphicsWidth) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(hullInfoFrame.Rect.Width, 0); } + if (hullInfoFrame.Rect.Bottom > GameMain.GraphicsHeight) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(0, hullInfoFrame.Rect.Height); } + + hullInfoFrame.Visible = true; + tooltipHeader.Text = header; + + tooltipFirstLine.Text = line1; + tooltipFirstLine.TextColor = line1Color ?? GUI.Style.TextColor; + + tooltipSecondLine.Text = line2; + tooltipSecondLine.TextColor = line2Color ?? GUI.Style.TextColor; + + tooltipThirdLine.Text = line3; + tooltipThirdLine.TextColor = line3Color ?? GUI.Style.TextColor; + } + + private void BakeSubmarine(Submarine sub, Rectangle container) + { + submarinePreview?.Dispose(); + Rectangle parentRect = new Rectangle(container.X, container.Y, container.Width, container.Height); + const int inflate = 128; + parentRect.Inflate(inflate, inflate); + RenderTarget2D rt = new RenderTarget2D(GameMain.Instance.GraphicsDevice, parentRect.Width, parentRect.Height, false, SurfaceFormat.Color, DepthFormat.None); + + using SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice); + GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); + GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + Rectangle worldBorders = sub.GetDockedBorders(); + worldBorders.Location += sub.WorldPosition.ToPoint(); + + parentRect.Inflate(-inflate, -inflate); + + foreach (MapEntity entity in subEntities) + { + if (entity is Structure wall) + { + if (wall.IsPlatform) { continue; } + DrawStructure(spriteBatch, wall, parentRect, worldBorders, inflate); + } + + if (entity is Item it) + { + if (it.GetComponent() != null || it.ParentInventory != null) { continue; } + DrawItem(spriteBatch, it, parentRect, worldBorders, inflate); + } + } + + spriteBatch.End(); + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); + submarinePreview = rt; + } + + private void DrawSubmarine(SpriteBatch spriteBatch) + { + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; + spriteBatch.End(); + if (submarinePreview is { } texture && miniMapContainer is { } mapContainer) + { + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, blendState: BlendState.NonPremultiplied, effect: GameMain.GameScreen.BlueprintEffect, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; + + GameMain.GameScreen.BlueprintEffect.Parameters["width"].SetValue((float)texture.Width); + GameMain.GameScreen.BlueprintEffect.Parameters["height"].SetValue((float)texture.Height); + + Color blueprintBlue = BlueprintBlue * currentMode switch { MiniMapMode.HullStatus => 0.1f, MiniMapMode.ElectricalView => 0.1f, _ => 0.5f }; + + Vector2 origin = new Vector2(texture.Width / 2f, texture.Height / 2f); + float scale = currentMode == MiniMapMode.HullStatus ? 1.0f : Zoom; + spriteBatch.Draw(texture, mapContainer.Center, null, blueprintBlue, 0f, origin, scale, SpriteEffects.None, 0f); + + spriteBatch.End(); + } + spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } + + private static void DrawItem(ISpriteBatch spriteBatch, Item item, Rectangle parent, Rectangle border, int inflate) + { + Sprite sprite = item.Sprite; + if (sprite is null) { return; } + + RectangleF entityRect = ScaleRectToUI(item, parent, border); + + Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); + Vector2 origin = new Vector2(sprite.Origin.X * spriteScale.X, sprite.Origin.Y * spriteScale.Y); + + if (item.GetComponent() is { } turret) + { + Vector2 drawPos = turret.GetDrawPos(); + drawPos.Y = -drawPos.Y; + if (turret.BarrelSprite is { } barrelSprite) + { + DrawAdditionalSprite(drawPos, barrelSprite, turret.Rotation + MathHelper.PiOver2); + } + } + + Vector2 pos = entityRect.Location + origin; + pos.X += inflate; + pos.Y += inflate; + + sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, MathHelper.ToRadians(item.Rotation), spriteScale, item.SpriteEffects); + + void DrawAdditionalSprite(Vector2 basePos, Sprite addSprite, float rotation) + { + RectangleF addRect = ScaleRectToUI(new RectangleF(basePos, addSprite.size * item.Scale), parent, border); + Vector2 addScale = new Vector2(addRect.Size.X / addSprite.size.X, addRect.Size.Y / addSprite.size.Y); + addSprite.Draw(spriteBatch, new Vector2(addRect.Location.X + inflate, addRect.Location.Y + inflate), item.SpriteColor, addSprite.Origin, rotation, addScale, item.SpriteEffects); + } + } + + private static void DrawStructure(ISpriteBatch spriteBatch, Structure structure, Rectangle parent, Rectangle border, int inflate) + { + Sprite sprite = structure.Sprite; + if (sprite is null) { return; } + + RectangleF entityRect = ScaleRectToUI(structure, parent, border); + Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y); + sprite.Draw(spriteBatch, new Vector2(entityRect.Location.X + inflate, entityRect.Location.Y + inflate), structure.SpriteColor, Vector2.Zero, 0f, spriteScale, structure.SpriteEffects); + } + + private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders) + { + return ScaleRectToUI(entity.WorldRect, parentRect, worldBorders); + } + + private static RectangleF ScaleRectToUI(RectangleF rect, RectangleF parentRect, RectangleF worldBorders) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, rect); + return relativeRect.RectangleRelativeTo(parentRect, skipOffset: true); + } + + private void DrawHullCards(SpriteBatch spriteBatch, Hull hull, HullData data, GUIComponent frame) + { + cardsToDraw.Clear(); + + if (GameMain.GameSession?.CrewManager is { ActiveOrders: { } orders }) + { + foreach (var pair in orders) + { + Order order = pair.First; + if (order is { SymbolSprite: { }, TargetEntity: Hull _ } && order.TargetEntity == hull) + { + cardsToDraw.Add(new MiniMapSprite(order)); + } + } + } + + foreach (IdCard card in data.Cards) + { + if (card.GetJob() is { Icon: { }} job) + { + cardsToDraw.Add(new MiniMapSprite(job)); + } + } + + if (!cardsToDraw.Any()) { return; } + + var (centerX, centerY) = frame.Center; + + const float padding = 8f; + float totalWidth = 0f; + + float parentWidth = submarineContainer.Rect.Width / 24f; + + int i = 0; + foreach (MiniMapSprite info in cardsToDraw) + { + float spriteSize = info.Sprite.size.X * (parentWidth / info.Sprite.size.X) + padding; + if (totalWidth + spriteSize > frame.Rect.Width) { break; } + + totalWidth += spriteSize; + i++; + } + + if (i > 0) { totalWidth -= padding; } + + float adjustedCenterX = centerX - totalWidth / 2f; + + float offset = 0; + int amount = 0; + + foreach (MiniMapSprite info in cardsToDraw) + { + Sprite sprite = info.Sprite; + float scale = parentWidth / sprite.size.X; + float spriteSize = sprite.size.X * scale; + float posX = adjustedCenterX + offset; + + if (posX + spriteSize > frame.Rect.X + frame.Rect.Width && amount > 0) + { + int amountLeft = cardsToDraw.Count - amount; + if (amountLeft > 0) + { + string text = $"+{amountLeft}"; // TODO localization + var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable + float maxWidth = Math.Max(sizeX, sizeY); + Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); + + UISprite icon = GUI.Style.IconOverflowIndicator; + + const int iconPadding = 4; + icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); + + GUI.DrawString(spriteBatch, drawPos, text, GUI.Style.TextColor, font: GUI.SubHeadingFont); + } + break; + } + + float halfSize = spriteSize / 2f; + if (i > 0) { offset += halfSize; } + Vector2 pos = new Vector2(adjustedCenterX + offset, centerY); + sprite.Draw(spriteBatch, pos, info.Color * 0.8f, scale: scale, origin: sprite.size / 2f); + offset += halfSize + padding; + amount++; + } + } + + public static void GetLinkedHulls(Hull hull, List linkedHulls) { foreach (var linkedEntity in hull.linkedTo) { @@ -335,5 +1364,273 @@ namespace Barotrauma.Items.Components } } } + + public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings) + { + return CreateMiniMap(sub, parent, settings, null, out _); + } + + public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings, IEnumerable? pointsOfInterest, out ImmutableDictionary elements) + { + if (settings.Equals(default(MiniMapSettings))) + { + throw new ArgumentException($"Provided {nameof(MiniMapSettings)} is not valid, did you mean {nameof(MiniMapSettings)}.{nameof(MiniMapSettings.Default)}?", nameof(settings)); + } + + Dictionary pointsOfInterestCollection = new Dictionary(); + + RectangleF worldBorders = sub.GetDockedBorders(); + worldBorders.Location += sub.WorldPosition; + + // create a container that has the same "aspect ratio" as the sub + float aspectRatio = worldBorders.Width / worldBorders.Height; + float parentAspectRatio = parent.Rect.Width / (float)parent.Rect.Height; + + const float elementPadding = 0.9f; + + Vector2 containerScale = parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio); + + GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null); + + ImmutableHashSet connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet(); + ImmutableArray hullList = ImmutableArray.Empty; + ImmutableDictionary> combinedHulls = ImmutableDictionary>.Empty; + + if (settings.CreateHullElements) + { + hullList = Hull.hullList.Where(IsPartofSub).ToImmutableArray(); + combinedHulls = CombinedHulls(hullList); + } + + // Make components for non-linked hulls + foreach (Hull hull in hullList.Where(IsStandaloneHull)) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, hull.WorldRect); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: "ScanLines", color: settings.ElementColor) + { + OutlineColor = settings.ElementColor, + OutlineThickness = 2, + UserData = hull + }; + + pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(hullFrame)); + } + + // Make components for linked hulls + foreach (var (mainHull, linkedHulls) in combinedHulls) + { + MiniMapHullData data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders); + + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, data.Bounds); + + float highestY = 0f, + highestX = 0f; + + foreach (var (r, _) in data.RectDatas) + { + float y = r.Y - -r.Height, + x = r.X; + + if (y > highestY) { highestY = y; } + if (x > highestX) { highestX = x; } + } + + Dictionary hullsAndFrames = new Dictionary(); + + foreach (var (snappredRect, hull) in data.RectDatas) + { + RectangleF rect = snappredRect; + rect.Height = -rect.Height; + rect.Y -= rect.Height; + + var (parentW, parentH) = hullContainer.Rect.Size.ToVector2(); + Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH); + Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: settings.ElementColor) + { + UserData = hull, + UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y) + }; + + hullsAndFrames.Add(hull, hullFrame); + } + + /* + * This exists because the rectangle of GUIComponents still uses Rectangle instead of RectangleF + * and because of rounding sometimes it creates 1px gaps between which looks nasty so we snap + * the rectangles together if they are 2 pixels apart or less. + */ + foreach (var (hull1, frame1) in hullsAndFrames) + { + Rectangle rect1 = frame1.Rect; + foreach (var (hull2, frame2) in hullsAndFrames) + { + if (hull2 == hull1) { continue; } + + Rectangle rect2 = frame2.Rect; + Point size = frame1.RectTransform.NonScaledSize; + + const int treshold = 2; + + int diffY = rect2.Top - rect1.Bottom; + int diffX = rect2.Left - rect1.Right; + + if (diffY <= treshold && diffY > 0) + { + size.Y += diffY; + } + + if (diffX <= treshold && diffX > 0) + { + size.X += diffX; + } + + frame1.RectTransform.NonScaledSize = size; + } + } + + GUICustomComponent linkedHullFrame = new GUICustomComponent(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, (spriteBatch, component) => + { + foreach (List list in data.Polygon) + { + spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.OutlineColor, 2f); + } + }, (deltaTime, component) => + { + if (component.Parent.Rect.Size != data.ParentSize) + { + data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders); + } + }) + { + UserData = hullsAndFrames.Values.ToHashSet(), + OutlineColor = settings.ElementColor, + CanBeFocused = false + }; + + foreach (var (hull, component) in hullsAndFrames) + { + pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(component, linkedHullFrame)); + } + } + + if (pointsOfInterest != null) + { + foreach (MapEntity entity in pointsOfInterest) + { + RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, entity.WorldRect); + + GUIFrame poiComponent = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: null) + { + CanBeFocused = false, + UserData = entity + }; + + pointsOfInterestCollection.Add(entity, new MiniMapGUIComponent(poiComponent)); + } + } + + elements = pointsOfInterestCollection.ToImmutableDictionary(); + + return hullContainer; + + bool IsPartofSub(MapEntity entity) + { + if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine)) { return false; } + return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true); + } + + bool IsStandaloneHull(Hull hull) + { + return !combinedHulls.ContainsKey(hull) && !combinedHulls.Values.Any(hh => hh.Contains(hull)); + } + } + + private static ImmutableDictionary> CombinedHulls(ImmutableArray hulls) + { + Dictionary> combinedHulls = new Dictionary>(); + + foreach (Hull hull in hulls) + { + if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; } + + List linkedHulls = new List(); + GetLinkedHulls(hull, linkedHulls); + + linkedHulls.Remove(hull); + + foreach (Hull linkedHull in linkedHulls) + { + if (!combinedHulls.ContainsKey(hull)) + { + combinedHulls.Add(hull, new HashSet()); + } + + combinedHulls[hull].Add(linkedHull); + } + } + + return combinedHulls.ToImmutableDictionary(pair => pair.Key, pair => pair.Value.ToImmutableArray()); + } + + private static MiniMapHullData ConstructHullPolygon(Hull mainHull, ImmutableArray linkedHulls, GUIComponent parent, RectangleF worldBorders) + { + Rectangle parentRect = parent.Rect; + + Dictionary rects = new Dictionary(); + Rectangle worldRect = mainHull.WorldRect; + worldRect.Y = -worldRect.Y; + + rects.Add(mainHull, worldRect); + + foreach (Hull hull in linkedHulls) + { + Rectangle rect = hull.WorldRect; + rect.Y = -rect.Y; + + worldRect = Rectangle.Union(worldRect, rect); + rects.Add(hull, rect); + } + + worldRect.Y = -worldRect.Y; + + List normalizedRects = new List(); + List hullRefs = new List(); + + foreach (var (hull, rect) in rects) + { + Rectangle wRect = rect; + wRect.Y = -wRect.Y; + + var (posX, posY, sizeX, sizeY) = new RelativeEntityRect(worldBorders, wRect); + + RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, sizeX * parentRect.Width, sizeY * parentRect.Height); + + normalizedRects.Add(newRect); + hullRefs.Add(hull); + } + + hullRefs.Reverse(); // I have no idea why this is required + + ImmutableArray snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1); + + List> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles); + + List> scaledPolygon = new List>(); + + foreach (List list in polygon) + { + // scale down the polygon just a tiny bit + var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list); + float sizeX = polySizeX - 1f, + sizeY = polySizeY - 1f; + + scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY))); + } + + return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 3877a2ccf..a6b0b74b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -142,6 +142,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + float rotationRad = MathHelper.ToRadians(item.Rotation); if (FlowPercentage < 0.0f) { foreach (var (position, emitter) in pumpOutEmitters) @@ -149,12 +150,13 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; } //only emit "pump out" particles when underwater - Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - float angle = 0.0f; + Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); + float angle = -rotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; - angle = MathHelper.Pi; + angle += MathHelper.Pi; } if (item.FlippedY) { @@ -170,11 +172,12 @@ namespace Barotrauma.Items.Components foreach (var (position, emitter) in pumpInEmitters) { Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; - float angle = 0.0f; + relativeParticlePos = MathUtils.RotatePoint(relativeParticlePos, item.FlippedX ? rotationRad : -rotationRad); + float angle = -rotationRad; if (item.FlippedX) { relativeParticlePos.X = -relativeParticlePos.X; - angle = MathHelper.Pi; + angle += MathHelper.Pi; } if (item.FlippedY) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index dbf89512b..44d0ad13c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -134,6 +134,14 @@ namespace Barotrauma.Items.Components private static string caveLabel; + + [Serialize(false, false)] + public bool RightLayout + { + get; + set; + } + private bool AllowUsingMineralScanner => HasMineralScanner && !isConnectedToSteering; @@ -316,7 +324,7 @@ namespace Barotrauma.Items.Components "", warningColor, GUI.LargeFont, Alignment.Center); // Setup layout for nav terminal - if (isConnectedToSteering) + if (isConnectedToSteering || RightLayout) { controlContainer.RectTransform.RelativeOffset = controlBoxOffset; controlContainer.RectTransform.SetPosition(Anchor.TopRight); @@ -446,13 +454,8 @@ namespace Barotrauma.Items.Components zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f; zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); } - - if (PlayerInput.KeyHit(InputType.Run)) - { - SonarModeSwitch.OnClicked(SonarModeSwitch, null); - } } - + float distort = 1.0f - item.Condition / item.MaxCondition; for (int i = sonarBlips.Count - 1; i >= 0; i--) { @@ -885,7 +888,7 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { - if (!aiTarget.Enabled) { continue; } + if (aiTarget.InDetectable) { continue; } if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) { continue; } if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) @@ -1239,7 +1242,7 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; - if (disruption <= 0.0f || !aiTarget.Enabled) { continue; } + if (disruption <= 0.0f || aiTarget.InDetectable) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } float disruptionDist = (float)Math.Sqrt(distSqr); @@ -1364,28 +1367,6 @@ namespace Barotrauma.Items.Components blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default); } } - - foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) - { - if (!MathUtils.CircleIntersectsRectangle(pingSource, range, ruin.Area)) continue; - - foreach (var ruinShape in ruin.RuinShapes) - { - foreach (RuinGeneration.Line wall in ruinShape.Walls) - { - float cellDot = Vector2.Dot( - Vector2.Normalize(ruinShape.Center - pingSource), - Vector2.Normalize((wall.A + wall.B) / 2.0f - ruinShape.Center)); - if (cellDot > 0) continue; - - CreateBlipsForLine( - wall.A, wall.B, - pingSource, transducerPos, - pingRadius, prevPingRadius, - 100.0f, 1000.0f, range, pingStrength, passive); - } - } - } } foreach (Item item in Item.ItemList) @@ -1634,7 +1615,7 @@ namespace Barotrauma.Items.Components void CalculateDistance() { - pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + pathFinder ??= new PathFinder(WayPoint.WayPointList, false); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); if (!path.Unreachable) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 1f32930b3..b66bcaa5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -888,6 +888,7 @@ namespace Barotrauma.Items.Components maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); + pathFinder = null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 4b6c91176..643b2042e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Items.Components var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), textArea.RectTransform, Anchor.CenterRight), "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) { - TextGetter = () => $"{(int)charge}/{(int)capacity} {kWmin} ({((int)MathUtils.Percentage(charge, capacity)).ToString()} %)" + TextGetter = () => $"{(int)Math.Round(charge)}/{(int)capacity} {kWmin} ({(int)Math.Round(MathUtils.Percentage(charge, capacity))} %)" }; if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs new file mode 100644 index 000000000..8c17e9d90 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Quality : ItemComponent + { + public override void AddTooltipInfo(ref string name, ref string description) + { + foreach (var statValue in statValues) + { + int roundedValue = (int)Math.Round(statValue.Value * qualityLevel * 100); + if (roundedValue == 0) { return; } + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString(), true) ?? statValue.Key.ToString()}"; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs new file mode 100644 index 000000000..6f6d3d740 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Items.Components +{ + partial class RemoteController : ItemComponent + { + public override void DrawHUD(SpriteBatch spriteBatch, Character character) + { + currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character); + } + + public override void UpdateHUD(Character character, float deltaTime, Camera cam) + { + currentTarget?.UpdateHUD(cam, character,deltaTime); + } + + public override void AddToGUIUpdateList(int order = 0) + { + currentTarget?.AddToGUIUpdateList(order: -1); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 0099e8bfb..fbf133ced 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -15,16 +15,23 @@ namespace Barotrauma.Items.Components public GUIButton SabotageButton { get; private set; } + public GUIButton TinkerButton { get; private set; } + private GUIProgressBar progressBar; - private List particleEmitters = new List(); + private GUITextBlock progressBarOverlayText; + + private GUILayoutGroup extraButtonContainer; + + private readonly List particleEmitters = new List(); //the corresponding particle emitter is active when the condition is within this range - private List particleEmitterConditionRanges = new List(); + private readonly List particleEmitterConditionRanges = new List(); private SoundChannel repairSoundChannel; private string repairButtonText, repairingText; private string sabotageButtonText, sabotagingText; + private string tinkerButtonText, tinkeringText; private FixActions requestStartFixAction; @@ -45,8 +52,24 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { - if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; - return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))); + if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) { return false; } + if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } + + float maxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(character); + if (item.Condition / maxRepairConditionMultiplier < RepairThreshold) { return true; } + + if (CurrentFixer == character) + { + float condition = item.Condition / item.MaxRepairConditionMultiplier; + float maxCondition = item.MaxCondition / item.MaxRepairConditionMultiplier; + if (condition < maxCondition * maxRepairConditionMultiplier) + { + return true; + } + } + if (IsTinkerable(character)) { return true; } + + return false; } partial void InitProjSpecific(XElement element) @@ -85,7 +108,7 @@ namespace Barotrauma.Items.Components } } - private void CreateGUI() + protected override void CreateGUI() { var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.75f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { @@ -120,6 +143,11 @@ namespace Barotrauma.Items.Components progressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), progressBarHolder.RectTransform), color: GUI.Style.Green, barSize: 0.0f, style: "DeviceProgressBar"); + progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + { + IgnoreLayoutGroups = true + }; + repairButtonText = TextManager.Get("RepairButton"); repairingText = TextManager.Get("Repairing"); RepairButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), progressBarHolder.RectTransform, Anchor.TopCenter), repairButtonText) @@ -135,9 +163,16 @@ namespace Barotrauma.Items.Components progressBarHolder.RectTransform.MinSize = RepairButton.RectTransform.MinSize; RepairButton.RectTransform.MinSize = new Point((int)(RepairButton.TextBlock.TextSize.X * 1.2f), RepairButton.RectTransform.MinSize.Y); + extraButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), isHorizontal: true) + { + IgnoreLayoutGroups = true, + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) + }; + sabotageButtonText = TextManager.Get("SabotageButton"); sabotagingText = TextManager.Get("Sabotaging"); - SabotageButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), sabotageButtonText, style: "GUIButtonSmall") + SabotageButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), sabotageButtonText, style: "GUIButtonSmall") { IgnoreLayoutGroups = true, Visible = false, @@ -148,6 +183,22 @@ namespace Barotrauma.Items.Components return true; } }; + + tinkerButtonText = TextManager.Get("TinkerButton", returnNull: true) ?? "Tinker"; + tinkeringText = TextManager.Get("Tinkering", returnNull: true) ?? "Tinkering"; + TinkerButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), tinkerButtonText, style: "GUIButtonSmall") + { + IgnoreLayoutGroups = true, + Visible = false, + OnClicked = (btn, obj) => + { + requestStartFixAction = FixActions.Tinker; + item.CreateClientEvent(this); + return true; + } + }; + + extraButtonContainer.RectTransform.MinSize = new Point(0, SabotageButton.RectTransform.MinSize.Y); } partial void UpdateProjSpecific(float deltaTime) @@ -176,6 +227,7 @@ namespace Barotrauma.Items.Components { case FixActions.Repair: case FixActions.Sabotage: + case FixActions.Tinker: StartRepairing(Character.Controlled, requestStartFixAction); requestStartFixAction = FixActions.None; break; @@ -211,10 +263,24 @@ namespace Barotrauma.Items.Components { IsActive = true; - progressBar.BarSize = item.Condition / item.MaxCondition; + float defaultMaxCondition = (item.MaxCondition / item.MaxRepairConditionMultiplier); + + progressBar.BarSize = item.Condition / defaultMaxCondition; progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green); - RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition; + if (item.Condition > defaultMaxCondition) + { + float extraCondition = item.MaxCondition * (item.MaxRepairConditionMultiplier - 1.0f); + progressBar.Color = ToolBox.GradientLerp((item.Condition - defaultMaxCondition) / extraCondition, GUI.Style.ColorReputationHigh, GUI.Style.ColorReputationVeryHigh); + progressBarOverlayText.Visible = true; + progressBarOverlayText.Text = $"{(int)Math.Round((item.Condition / defaultMaxCondition) * 100)}%"; + } + else + { + progressBarOverlayText.Visible = false; + } + + RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition && item.ConditionPercentage < RepairThreshold; RepairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? repairButtonText : repairingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); @@ -226,7 +292,18 @@ namespace Barotrauma.Items.Components sabotageButtonText : sabotagingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + TinkerButton.Visible = IsTinkerable(character); + TinkerButton.IgnoreLayoutGroups = !TinkerButton.Visible; + TinkerButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Tinker)) && CanTinker(character); + TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker) ? + tinkerButtonText : + tinkeringText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); + System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); + + extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible; + extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.Visible; + foreach (GUIComponent c in GuiFrame.GetChild(0).Children) { if (!(c.UserData is Skill skill)) continue; @@ -278,9 +355,12 @@ namespace Barotrauma.Items.Components deteriorationTimer = msg.ReadSingle(); deteriorateAlwaysResetTimer = msg.ReadSingle(); DeteriorateAlways = msg.ReadBoolean(); + tinkeringDuration = msg.ReadSingle(); + tinkeringStrength = msg.ReadSingle(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index f68e5f37d..25d45c773 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -31,6 +31,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize("0.5,0.5)", false)] + public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); + public Vector2 DrawSize { get @@ -57,7 +60,6 @@ namespace Barotrauma.Items.Components sourcePos = sourceLimb.body.DrawPosition; } return sourcePos; - } partial void InitProjSpecific(XElement element) @@ -81,13 +83,15 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (target == null) { return; } + if (target == null || target.Removed) { return; } + if (target.ParentInventory != null) { return; } Vector2 startPos = GetSourcePos(); startPos.Y = -startPos.Y; if (source is Item sourceItem) { - var turret = sourceItem?.GetComponent(); + var turret = sourceItem.GetComponent(); + var weapon = sourceItem.GetComponent(); if (turret != null) { startPos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, -(sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y)); @@ -96,8 +100,21 @@ namespace Barotrauma.Items.Components startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } } + else if (weapon != null) + { + Vector2 barrelPos = FarseerPhysics.ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); + barrelPos.Y = -barrelPos.Y; + startPos += barrelPos; + } } - Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); + Vector2 endPos = new Vector2(target.DrawPosition.X, target.DrawPosition.Y); + Vector2 flippedPos = target.Sprite.size * target.Scale * (Origin - new Vector2(0.5f)); + if (target.body.Dir < 0.0f) + { + flippedPos.X = -flippedPos.X; + } + endPos += Vector2.Transform(flippedPos, Matrix.CreateRotationZ(target.body.Rotation)); + endPos.Y = -endPos.Y; if (Snapped) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..a44dca68e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -0,0 +1,29 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent, IServerSerializable + { + partial void UpdateProjSpecific() + { + if (Holdable != null && Holdable.Attached && (AlwaysDisplayProgressBar || DisplayProgressBar) && !IsScanCompleted) + { + Character.Controlled?.UpdateHUDProgressBar(this, + item.WorldPosition, + ScanTimer / ScanDuration, + GUI.Style.Red, GUI.Style.Green, + textTag: "progressbar.scanning"); + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + bool wasScanCompletedPreviously = IsScanCompleted; + scanTimer = msg.ReadSingle(); + if (!wasScanCompletedPreviously && IsScanCompleted) + { + OnScanCompleted?.Invoke(this); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..ff50d0fef --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,115 @@ +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable + { + private string[] terminalButtonStyles; + private GUIFrame containerHolder; + private GUIImage containerIndicator; + private GUIComponentStyle indicatorStyleRed, indicatorStyleGreen; + + partial void InitProjSpecific(XElement element) + { + terminalButtonStyles = new string[RequiredSignalCount]; + int i = 0; + foreach (var childElement in element.GetChildElements("TerminalButton")) + { + string style = childElement.GetAttributeString("style", null); + if (style == null) { continue; } + terminalButtonStyles[i++] = style; + } + indicatorStyleRed = GUI.Style.GetComponentStyle("IndicatorLightRed"); + indicatorStyleGreen = GUI.Style.GetComponentStyle("IndicatorLightGreen"); + CreateGUI(); + } + + protected override void CreateGUI() + { + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), GuiFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.08f + }; + paddedFrame.OnAddedToGUIUpdateList += (component) => + { + bool buttonsEnabled = AllowUsingButtons; + foreach (var child in component.Children) + { + if (!(child is GUIButton)) { continue; } + if (!(child.UserData is int)) { continue; } + child.Enabled = buttonsEnabled; + child.Children.ForEach(c => c.Enabled = buttonsEnabled); + } + bool itemsContained = Container.Inventory.AllItems.Any(); + if (itemsContained) + { + var indicatorStyle = buttonsEnabled ? indicatorStyleGreen : indicatorStyleRed; + if (containerIndicator.Style != indicatorStyle) + { + containerIndicator.ApplyStyle(indicatorStyle); + } + } + containerIndicator.OverrideState = itemsContained ? GUIComponent.ComponentState.Selected : GUIComponent.ComponentState.None; + }; + + float x = 1.0f / (1 + RequiredSignalCount); + float y = Math.Min((x * paddedFrame.Rect.Width) / paddedFrame.Rect.Height, 0.5f); + Vector2 relativeSize = new Vector2(x, y); + + var containerSection = new GUIFrame(new RectTransform(new Vector2(x, 1.0f), paddedFrame.RectTransform), style: null); + var containerSlot = new GUIFrame(new RectTransform(new Vector2(1.0f, y), containerSection.RectTransform, anchor: Anchor.Center), style: null); + containerHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.2f), containerSlot.RectTransform, Anchor.BottomCenter), style: null); + containerIndicator = new GUIImage(new RectTransform(new Vector2(0.5f, 0.5f * (1.0f - y)), containerSection.RectTransform, anchor: Anchor.BottomCenter), + style: "IndicatorLightRed", scaleToFit: true); + + for (int i = 0; i < RequiredSignalCount; i++) + { + var button = new GUIButton(new RectTransform(relativeSize, paddedFrame.RectTransform), style: null) + { + UserData = i, + OnClicked = (button, userData) => + { + if (GameMain.IsSingleplayer) + { + SendSignal((int)userData); + } + else + { + item.CreateClientEvent(this, new object[] { userData }); + } + return true; + } + }; + var image = new GUIImage(new RectTransform(Vector2.One, button.RectTransform), terminalButtonStyles[i], scaleToFit: true); + } + } + + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + OnItemLoadedProjSpecific(); + } + + partial void OnItemLoadedProjSpecific() + { + Container.AllowUIOverlap = true; + Container.Inventory.RectTransform = containerHolder.RectTransform; + } + + public void ClientWrite(IWriteMessage msg, object[] extraData = null) + { + Write(msg, extraData); + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), isServerMessage: true); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 4efc832fc..336769480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -54,6 +54,11 @@ namespace Barotrauma.Items.Components if (wireComponent != null) { equippedWire = wireComponent; + var connectedEnd = equippedWire.OtherConnection(null); + if (connectedEnd?.Item.Submarine != null && panel.Item.Submarine != connectedEnd.Item.Submarine) + { + equippedWire = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 1162e4bff..72e8482da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -88,11 +88,6 @@ namespace Barotrauma.Items.Components return character == Character.Controlled && character == user && character.SelectedConstruction == item; } - public override void AddToGUIUpdateList() - { - GuiFrame?.AddToGUIUpdateList(); - } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) { if (character != Character.Controlled || character != user || character.SelectedConstruction != item) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 353e4550b..a816f402a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -63,11 +63,11 @@ namespace Barotrauma.Items.Components } OutputValue = input; - ShowOnDisplay(input); + ShowOnDisplay(input, addToHistory: true); item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input, bool addToHistory = true) + partial void ShowOnDisplay(string input, bool addToHistory) { if (addToHistory) { @@ -85,7 +85,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), "> " + input, - textColor: Color.LimeGreen, wrap: true) + textColor: Color.LimeGreen, wrap: true, font: UseMonospaceFont ? GUI.MonospacedFont : GUI.GlobalFont) { CanBeFocused = false }; @@ -118,9 +118,9 @@ namespace Barotrauma.Items.Components // This method is overrided instead of the UpdateHUD method because this ensures the input box is selected // even when the terminal component is selected for the very first time. Doing the input box selection in the // UpdateHUD method only selects the input box on every terminal selection except for the very first time. - public override void AddToGUIUpdateList() + public override void AddToGUIUpdateList(int order = 0) { - base.AddToGUIUpdateList(); + base.AddToGUIUpdateList(order: order); if (shouldSelectInputBox) { inputBox.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 04313fee8..d974c2044 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -39,6 +39,34 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, false)] + public bool ThermalGoggles + { + get; + private set; + } + + [Serialize(true, false)] + public bool ShowDeadCharacters + { + get; + private set; + } + + [Serialize(true, false)] + public bool ShowTexts + { + get; + private set; + } + + [Serialize("72,119,72,120", false)] + public Color OverlayColor + { + get; + private set; + } + private readonly List visibleCharacters = new List(); private const float UpdateInterval = 0.5f; @@ -48,6 +76,8 @@ namespace Barotrauma.Items.Components private bool isEquippable; + private float thermalEffectState; + public IEnumerable VisibleCharacters { get @@ -80,7 +110,10 @@ namespace Barotrauma.Items.Components { refEntity = item; } - + + thermalEffectState += deltaTime; + thermalEffectState %= 10000.0f; + if (updateTimer > 0.0f) { updateTimer -= deltaTime; @@ -91,6 +124,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c == equipper || !c.Enabled || c.Removed) { continue; } + if (!ShowDeadCharacters && c.IsDead) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) @@ -123,27 +157,71 @@ namespace Barotrauma.Items.Components { if (character == null) { return; } - GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), - Color.LightGreen * 0.5f); - - Character closestCharacter = null; - float closestDist = float.PositiveInfinity; - foreach (Character c in visibleCharacters) + if (OverlayColor.A > 0) { - if (c == character || !c.Enabled || c.Removed) { continue; } - - float dist = Vector2.DistanceSquared(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), c.WorldPosition); - if (dist < closestDist) - { - closestCharacter = c; - closestDist = dist; - } + GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), OverlayColor); } - if (closestCharacter != null) + if (ShowTexts) { - float dist = Vector2.Distance(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), closestCharacter.WorldPosition); - DrawCharacterInfo(spriteBatch, closestCharacter, 1.0f - MathHelper.Max((dist - (Range - FadeOutRange)) / FadeOutRange, 0.0f)); + Character closestCharacter = null; + float closestDist = float.PositiveInfinity; + foreach (Character c in visibleCharacters) + { + if (c == character || !c.Enabled || c.Removed) { continue; } + + float dist = Vector2.DistanceSquared(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), c.WorldPosition); + if (dist < closestDist) + { + closestCharacter = c; + closestDist = dist; + } + } + + if (closestCharacter != null) + { + float dist = Vector2.Distance(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), closestCharacter.WorldPosition); + DrawCharacterInfo(spriteBatch, closestCharacter, 1.0f - MathHelper.Max((dist - (Range - FadeOutRange)) / FadeOutRange, 0.0f)); + } + } + + if (ThermalGoggles) + { + spriteBatch.End(); + GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(Color.Red.ToVector4() * (0.3f + MathF.Sin(thermalEffectState) * 0.05f)); + GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"]; + GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.01f + MathF.Sin(thermalEffectState) * 0.005f); + GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect); + + Entity refEntity = equipper; + if (!isEquippable || refEntity == null) + { + refEntity = item; + } + + foreach (Character c in Character.CharacterList) + { + if (c == character || !c.Enabled || c.Removed || c.Params.HideInThermalGoggles) { continue; } + if (!ShowDeadCharacters && c.IsDead) { continue; } + + float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); + if (dist > Range * Range) { continue; } + + Sprite pingCircle = GUI.Style.UIThermalGlow.Sprite; + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.Mass < 1.0f) { continue; } + float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f); + float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f); + Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); + Vector2 drawPos = new Vector2(limb.body.DrawPosition.X + (noise1 - 0.5f) * 100, -limb.body.DrawPosition.Y + (noise2 - 0.5f) * 100); + pingCircle.Draw(spriteBatch, drawPos, 0.0f, scale: Math.Max(spriteScale.X, spriteScale.Y)); + } + } + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index f28cdf48b..f4ba37137 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -177,6 +177,10 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { recoilTimer = RetractionTime; + if (user != null) + { + recoilTimer /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed); + } PlaySound(ActionType.OnUse); Vector2 particlePos = GetRelativeFiringPosition(UseFiringOffsetForMuzzleFlash); foreach (ParticleEmitter emitter in particleEmitters) @@ -534,20 +538,20 @@ namespace Barotrauma.Items.Components minRotationWidget.Draw(spriteBatch, (float)Timing.Step); maxRotationWidget.Draw(spriteBatch, (float)Timing.Step); - Vector2 GetDrawPos() - { - Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); - if (item.Submarine != null) { drawPos += item.Submarine.DrawPosition; } - drawPos.Y = -drawPos.Y; - return drawPos; - } - void UpdateBarrel() { rotation = (minRotation + maxRotation) / 2; } } + public Vector2 GetDrawPos() + { + Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); + if (item.Submarine != null) { drawPos += item.Submarine.DrawPosition; } + drawPos.Y = -drawPos.Y; + return drawPos; + } + private Widget GetWidget(string id, SpriteBatch spriteBatch, int size = 5, float thickness = 1f, Action initMethod = null) { Vector2 offset = new Vector2(size / 2 + 5, -10); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 9692249c8..10d0fc376 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -5,17 +5,17 @@ namespace Barotrauma.Items.Components { partial class Wearable { - private void GetDamageModifierText(ref string description, float damageMultiplier, string afflictionIdentifier) + private void GetDamageModifierText(ref string description, DamageModifier damageModifier, string afflictionIdentifier) { - int roundedValue = (int)Math.Round((1 - damageMultiplier) * 100); + int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? afflictionIdentifier}"; } - public override void AddTooltipInfo(ref string description) + public override void AddTooltipInfo(ref string name, ref string description) { - if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f)) || SkillModifiers.Any()) + if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f) || !MathUtils.NearlyEqual(d.ProbabilityMultiplier, 1f)) || SkillModifiers.Any()) { description += "\n"; } @@ -31,11 +31,11 @@ namespace Barotrauma.Items.Components foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) { - GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); } foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionTypes) { - GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index e563f152d..a8d965902 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -297,9 +297,10 @@ namespace Barotrauma } } + string name = item.Name; foreach (ItemComponent component in item.Components) { - component.AddTooltipInfo(ref description); + component.AddTooltipInfo(ref name, ref description); } if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null) @@ -315,12 +316,18 @@ namespace Barotrauma string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); - toolTip = $"‖color:{colorStr}‖{item.Name}‖color:end‖"; + toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; + if (item.GetComponent() != null) + { + // substring by to get rid of the empty space at start, text file should be adjusted + toolTip += $"\n{TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "", fallBackTag: "itemname.quality3")?.Substring(1)}"; + } + if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) { toolTip += " " + TextManager.Get("connectionlocked"); } - if (!item.IsFullCondition && !item.Prefab.HideConditionBar) + if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip) { string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull)); toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; @@ -478,7 +485,7 @@ namespace Barotrauma } if (container == null) { return false; } - return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); + return owner.SelectedCharacter != null|| (!(owner is Character character)) || !container.KeepOpenWhenEquippedBy(character) || !owner.HasEquippedItem(container.Item); } protected virtual bool HideSlot(int i) @@ -594,7 +601,10 @@ namespace Barotrauma { var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, slots[slotIndex].FirstOrDefault()?.GetComponent()?.Inventory); if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && slotRef.ParentInventory is CharacterInventory) { return; } - selectedSlot = slotRef; + if (CanSelectSlot(slotRef)) + { + selectedSlot = slotRef; + } } if (!DraggingItems.Any()) @@ -667,6 +677,10 @@ namespace Barotrauma if (subInventory.visualSlots == null) { subInventory.CreateSlots(); } canMove = container.MovableFrame && !subInventory.IsInventoryHoverAvailable(Owner as Character, container) && subInventory.originalPos != Point.Zero; + if (this is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default) + { + canMove = false; + } if (canMove) { @@ -715,7 +729,7 @@ namespace Barotrauma spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); } - int columns = (int)Math.Max(Math.Floor(Math.Sqrt(itemCapacity)), 1); + int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) { columns++; @@ -826,11 +840,23 @@ namespace Barotrauma return rect.Contains(PlayerInput.MousePosition); } + public static bool IsMouseOnInventory + { + get; private set; + } + + /// + /// Refresh the value of IsMouseOnInventory + /// + public static void RefreshMouseOnInventory() + { + IsMouseOnInventory = DetermineMouseOnInventory(); + } + /// /// Is the mouse on any inventory element (slot, equip button, subinventory...) /// - /// - public static bool IsMouseOnInventory(bool ignoreDraggedItem = false) + private static bool DetermineMouseOnInventory(bool ignoreDraggedItem = false) { if (GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI)) @@ -1112,7 +1138,7 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); - if (!IsMouseOnInventory(ignoreDraggedItem: true) && + if (!DetermineMouseOnInventory(ignoreDraggedItem: true) && CharacterHealth.OpenHealthWindow != null) { bool dropSuccessful = false; @@ -1279,34 +1305,52 @@ namespace Barotrauma DraggingItems.Clear(); } - if (selectedSlot != null) + if (selectedSlot != null && !CanSelectSlot(selectedSlot)) { - if (!selectedSlot.Slot.MouseOn()) + selectedSlot = null; + } + } + + private static bool CanSelectSlot(SlotReference selectedSlot) + { + if (!selectedSlot.Slot.MouseOn()) + { + return false; + } + else + { + var rootOwner = (selectedSlot.ParentInventory?.Owner as Item)?.GetRootInventoryOwner(); + if (selectedSlot.ParentInventory?.Owner != Character.Controlled && + selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedCharacter && + selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedConstruction && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(selectedSlot.ParentInventory?.Owner) ?? false) && + rootOwner != Character.Controlled && + rootOwner != Character.Controlled.SelectedCharacter && + rootOwner != Character.Controlled.SelectedConstruction && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(rootOwner) ?? false)) { - selectedSlot = null; + return false; } - else + var parentItem = (selectedSlot?.ParentInventory?.Owner as Item) ?? selectedSlot?.Item; + if ((parentItem?.GetRootInventoryOwner() is Character ownerCharacter) && + ownerCharacter == Character.Controlled && + CharacterHealth.OpenHealthWindow?.Character != ownerCharacter && + ownerCharacter.Inventory.IsInLimbSlot(parentItem, InvSlotType.HealthInterface)) { - var rootOwner = (selectedSlot.ParentInventory?.Owner as Item)?.GetRootInventoryOwner(); - if (selectedSlot.ParentInventory?.Owner != Character.Controlled && - selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedCharacter && - selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedConstruction && - !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(selectedSlot.ParentInventory?.Owner) ?? false) && - rootOwner != Character.Controlled && - rootOwner != Character.Controlled.SelectedCharacter && - rootOwner != Character.Controlled.SelectedConstruction && - !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(rootOwner) ?? false)) - { - selectedSlot = null; - } + highlightedSubInventorySlots.RemoveWhere(s => s.Item == parentItem); + return false; } } + return true; } + protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable() || Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) + if (!subSlot.Inventory.Movable() || + (Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) || + (subSlot.ParentInventory is CharacterInventory characterInventory && characterInventory.CurrentLayout != CharacterInventory.Layout.Default)) { hoverArea = subSlot.Slot.Rect; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); @@ -1375,7 +1419,7 @@ namespace Barotrauma float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); Vector2 itemPos = PlayerInput.MousePosition; - bool mouseOnHealthInterface = CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement; + bool mouseOnHealthInterface = CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.MouseOnElement && DraggingItems.Any(it => it.UseInHealthInterface); if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { @@ -1434,7 +1478,8 @@ namespace Barotrauma } Color slotColor = Color.White; - if (inventory?.Owner is Item i && !i.IsPlayerTeamInteractable) { slotColor = Color.Gray; } + Item parentItem = inventory?.Owner as Item; + if (parentItem != null && !parentItem.IsPlayerTeamInteractable) { slotColor = Color.Gray; } var itemContainer = item?.GetComponent(); if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null)) { @@ -1513,7 +1558,7 @@ namespace Barotrauma var indicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); Sprite indicatorSprite = indicatorStyle?.GetDefaultSprite(); Sprite emptyIndicatorSprite = indicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover); - DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition); + DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition); } if (itemContainer != null && itemContainer.ShowContainedStateIndicator) @@ -1525,14 +1570,14 @@ namespace Barotrauma } else { - var containedItem = itemContainer.Inventory.slots[0].FirstOrDefault(); - containedState = itemContainer.Inventory.Capacity == 1 ? + var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; if (containedItem != null && itemContainer.Inventory.Capacity == 1) { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.MaxStackSize); - if (maxStackSize > 1) + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) { containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; } @@ -1556,6 +1601,27 @@ namespace Barotrauma DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, containedIndicatorArea, containedState, pulsate: !usingDefaultSprite && containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item)); } + + if (item.Quality != 0) + { + var style = GUI.Style.GetComponentStyle("InnerGlowSmall"); + if (style == null) + { + GUI.DrawRectangle(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.7f); + } + else + { + style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.5f); + } + } + } + else + { + var slotIcon = parentItem?.GetComponent()?.GetSlotIcon(slotIndex); + if (slotIcon != null) + { + slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUI.Style.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f); + } } } @@ -1590,7 +1656,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 (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface) + if (CharacterHealth.OpenHealthWindow != null && !item.UseInHealthInterface && !item.AllowedSlots.Contains(InvSlotType.HealthInterface) && item.GetComponent() == null) { spriteColor = Color.Lerp(spriteColor, Color.TransparentBlack, 0.5f); } @@ -1611,9 +1677,9 @@ namespace Barotrauma scale: iconSize.X / stealIcon.size.X); } int maxStackSize = item.Prefab.MaxStackSize; - if (item.Container != null) + if (inventory is ItemInventory itemInventory) { - maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.MaxStackSize ?? maxStackSize); + maxStackSize = Math.Min(maxStackSize, itemInventory.Container.GetMaxStackSize(slotIndex)); } if (maxStackSize > 1 && inventory != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index d675203fd..900731c83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -276,7 +276,7 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - float displayCondition = FakeBroken ? 0.0f : condition; + float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; Vector2 drawOffset = Vector2.Zero; if (displayCondition < MaxCondition) { @@ -326,13 +326,18 @@ namespace Barotrauma size, color: color, textureScale: Vector2.One * Scale, depth: depth); - fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, - textureScale: Vector2.One * Scale, - depth: depth - 0.000001f); + + if (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, + textureScale: Vector2.One * Scale, + depth: d); + } foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteBatch, @@ -357,7 +362,11 @@ namespace Barotrauma if (color.A > 0) { activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); - fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, depth - 0.000001f); + if (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, d); + } } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { @@ -368,7 +377,7 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, @@ -410,8 +419,11 @@ namespace Barotrauma } } body.Draw(spriteBatch, activeSprite, color, depth, Scale); - if (fadeInBrokenSprite != null) { body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, depth - 0.000001f, Scale); } - + if (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); + } foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -464,6 +476,11 @@ namespace Barotrauma if (GameMain.DebugDraw) { body?.DebugDraw(spriteBatch, Color.White); + if (GetComponent()?.PhysicsBody is PhysicsBody triggerBody) + { + triggerBody.UpdateDrawPosition(); + triggerBody.DebugDraw(spriteBatch, Color.White); + } } if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space)) @@ -704,6 +721,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), + Enabled = Prefab.CanFlipX, OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) @@ -717,6 +735,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), + Enabled = Prefab.CanFlipY, OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) @@ -1068,7 +1087,7 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter != character && - otherCharacter.SelectedConstruction == character.SelectedConstruction) + otherCharacter.SelectedConstruction == this) { ItemInUseWarning.Visible = true; if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); } @@ -1180,7 +1199,7 @@ namespace Barotrauma return texts; } - public override void AddToGUIUpdateList() + public override void AddToGUIUpdateList(int order = 0) { if (Screen.Selected is SubEditorScreen) { @@ -1194,7 +1213,14 @@ namespace Barotrauma } } - if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this) { return; } + if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this && GetComponent() == null) + { + if (Character.Controlled.SelectedConstruction?.GetComponent()?.TargetItem != this && + !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) + { + return; + } + } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) @@ -1205,7 +1231,7 @@ namespace Barotrauma bool wasUsingAlternativeLayout = ic.UseAlternativeLayout; ic.UseAlternativeLayout = useAlternativeLayout; needsLayoutUpdate |= ic.UseAlternativeLayout != wasUsingAlternativeLayout; - ic.AddToGUIUpdateList(); + ic.AddToGUIUpdateList(order); } if (itemInUseWarning != null && itemInUseWarning.Visible) @@ -1530,6 +1556,7 @@ namespace Barotrauma byte bodyType = msg.ReadByte(); bool spawnedInOutpost = msg.ReadBoolean(); bool allowStealing = msg.ReadBoolean(); + int quality = msg.ReadRangedInteger(0, Items.Components.Quality.MaxQuality); byte teamID = msg.ReadByte(); bool tagsChanged = msg.ReadBoolean(); string tags = ""; @@ -1605,7 +1632,8 @@ namespace Barotrauma item = new Item(itemPrefab, pos, sub, id: itemId) { SpawnedInOutpost = spawnedInOutpost, - AllowStealing = allowStealing + AllowStealing = allowStealing, + Quality = quality }; } catch (Exception e) @@ -1623,6 +1651,10 @@ namespace Barotrauma { wifiComponent.TeamID = (CharacterTeamType)teamID; } + foreach (IdCard idCard in item.GetComponents()) + { + idCard.TeamID = (CharacterTeamType)teamID; + } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } var nameTag = item.GetComponent(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 5854fc17b..d81332c48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -77,6 +77,13 @@ namespace Barotrauma protected set; } + [Serialize(true, false)] + public bool ShowInStatusMonitor + { + get; + private set; + } + [Serialize("", false)] public string ImpactSoundTag { get; private set; } @@ -84,13 +91,13 @@ namespace Barotrauma public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - + if (PlayerInput.SecondaryMouseButtonClicked()) { selected = null; return; } - + var potentialContainer = MapEntity.GetPotentialContainer(position); if (!ResizeHorizontal && !ResizeVertical) @@ -155,7 +162,7 @@ namespace Barotrauma { potentialContainer.IsHighlighted = true; } - + //if (PlayerInput.GetMouseState.RightButton == ButtonState.Pressed) selected = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 308010cac..57d7b3e44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -8,6 +8,8 @@ namespace Barotrauma { partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull) { + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + if (shockwave) { GameMain.ParticleManager.CreateParticle("shockwave", worldPosition, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 2138e1109..13533eb7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -676,7 +676,7 @@ namespace Barotrauma } else { - remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), i, colorStrength, color, 0)); + remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); } } paintAmount = BackgroundSections.Sum(s => s.ColorStrength); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs index 6238f2d5d..bac02e6b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs @@ -7,14 +7,9 @@ namespace Barotrauma.RuinGeneration { public void DebugDraw(SpriteBatch spriteBatch) { - foreach (RuinShape shape in allShapes) - { - GUI.DrawString(spriteBatch, new Vector2(shape.Center.X, -shape.Center.Y - 50), shape.DistanceFromEntrance.ToString(), Color.White, Color.Black * 0.5f, font: GUI.LargeFont); - } - foreach (Line line in walls) - { - GUI.DrawLine(spriteBatch, new Vector2(line.A.X, -line.A.Y), new Vector2(line.B.X, -line.B.Y), GUI.Style.Red, 0.0f, 10); - } + Rectangle drawRect = Area; + drawRect.Y = -drawRect.Y - Area.Height; + GUI.DrawRectangle(spriteBatch, drawRect, Color.Cyan, false, 0, 6); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 5c2fa3df5..eccf97ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -846,16 +846,15 @@ namespace Barotrauma.Lights if (chList.Submarine == null) { list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); - } //light is outside, convexhull inside a sub else { Rectangle subBorders = chList.Submarine.Borders; subBorders.Y -= chList.Submarine.Borders.Height; - if (!MathUtils.CircleIntersectsRectangle(lightPos - chList.Submarine.WorldPosition, range, subBorders)) continue; + if (!MathUtils.CircleIntersectsRectangle(lightPos - chList.Submarine.WorldPosition, range, subBorders)) { continue; } - lightPos -= (chList.Submarine.WorldPosition - chList.Submarine.HiddenSubPosition); + lightPos -= chList.Submarine.WorldPosition - chList.Submarine.HiddenSubPosition; list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } @@ -865,14 +864,6 @@ namespace Barotrauma.Lights //light is inside, convexhull outside if (chList.Submarine == null) { - lightPos += (ParentSub.WorldPosition - ParentSub.HiddenSubPosition); - HashSet visibleRuins = new HashSet(); - foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) - { - if (!MathUtils.CircleIntersectsRectangle(lightPos, range, ruin.Area)) { continue; } - visibleRuins.Add(ruin); - } - list.AddRange(chList.List.FindAll(ch => ch.ParentEntity?.ParentRuin != null && visibleRuins.Contains(ch.ParentEntity.ParentRuin))); continue; } //light and convexhull are both inside the same sub diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index bb8e93052..8ad1a0596 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -126,8 +126,8 @@ namespace Barotrauma.Lights } LosTexture?.Dispose(); - LosTexture = new RenderTarget2D(graphics, - (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), + LosTexture = new RenderTarget2D(graphics, + (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), (int)(GameMain.GraphicsHeight * GameMain.Config.LightMapScale), false, SurfaceFormat.Color, DepthFormat.None); } @@ -183,7 +183,7 @@ namespace Barotrauma.Lights activeLights.Clear(); foreach (LightSource light in lights) { - if (!light.Enabled) { continue; } + if (!light.Enabled) { continue; } if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if (light.ParentBody != null) { @@ -197,7 +197,9 @@ namespace Barotrauma.Lights float spriteRange = Math.Max( light.LightSprite.size.X * light.SpriteScale.X * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.X - 0.5f)), light.LightSprite.size.Y * light.SpriteScale.Y * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.Y - 0.5f))); - range = Math.Max(spriteRange, range); + + float targetSize = Math.Max(light.LightTextureTargetSize.X, light.LightTextureTargetSize.Y); + range = Math.Max(Math.Max(spriteRange, targetSize), range); } if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, range, viewRect)) { continue; } activeLights.Add(light); @@ -238,16 +240,16 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - if (backgroundObstructor != null) + /*if (backgroundObstructor != null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); spriteBatch.End(); - } + }*/ spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); - Dictionary visibleHulls = GetVisibleHulls(cam); + Dictionary visibleHulls = GetVisibleHulls(cam); foreach (KeyValuePair hull in visibleHulls) { GUI.DrawRectangle(spriteBatch, @@ -258,18 +260,18 @@ namespace Barotrauma.Lights spriteBatch.End(); SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(AmbientLight.ToVector4()); + SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); graphics.BlendState = BlendState.Additive; - + //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); foreach (LightSource light in activeLights) { //don't draw limb lights at this point, they need to be drawn after lights have been obstructed by characters @@ -294,8 +296,8 @@ namespace Barotrauma.Lights { if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { @@ -304,7 +306,7 @@ namespace Barotrauma.Lights } } spriteBatch.End(); - + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); @@ -312,8 +314,8 @@ namespace Barotrauma.Lights { if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; } - Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? - Color.Black : + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { @@ -343,9 +345,9 @@ namespace Barotrauma.Lights } lightEffect.World = transform; - + GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); - + if (Character.Controlled != null) { DrawHalo(Character.Controlled); @@ -412,7 +414,7 @@ namespace Barotrauma.Lights } } if (highlightedEntities.Count == 0) { return false; } - + //draw characters in light blue first graphics.SetRenderTarget(HighlightMap); SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; @@ -484,9 +486,9 @@ namespace Barotrauma.Lights //raster pattern on top of everything spriteBatch.Begin(blendState: BlendState.NonPremultiplied, samplerState: SamplerState.LinearWrap); - spriteBatch.Draw(highlightRaster, - new Rectangle(0, 0, HighlightMap.Width, HighlightMap.Height), - new Rectangle(0, 0, (int)(HighlightMap.Width / currLightMapScale * 0.5f), (int)(HighlightMap.Height / currLightMapScale * 0.5f)), + spriteBatch.Draw(highlightRaster, + new Rectangle(0, 0, HighlightMap.Width, HighlightMap.Height), + new Rectangle(0, 0, (int)(HighlightMap.Width / currLightMapScale * 0.5f), (int)(HighlightMap.Height / currLightMapScale * 0.5f)), Color.White * 0.5f); spriteBatch.End(); @@ -542,7 +544,7 @@ namespace Barotrauma.Lights { graphics.Clear(Color.White); } - + //-------------------------------------- @@ -595,9 +597,9 @@ namespace Barotrauma.Lights } } } - graphics.SetRenderTarget(null); + graphics.SetRenderTarget(null); } - + public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 2e44b1875..a47285cca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -104,13 +104,13 @@ namespace Barotrauma.Lights blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); } } - + public float TextureRange { get; private set; } - + public Sprite OverrideLightTexture { get; @@ -137,7 +137,7 @@ namespace Barotrauma.Lights public LightSourceParams(XElement element) { Deserialize(element); - + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -206,9 +206,9 @@ namespace Barotrauma.Lights private short[] indices; private List hullsInRange; - + public Texture2D texture; - + public SpriteEffects LightSpriteEffect; public Submarine ParentSub; @@ -224,7 +224,7 @@ namespace Barotrauma.Lights private float prevCalculatedRange; private Vector2 prevCalculatedPosition; - //do we need to recheck which convex hulls are within range + //do we need to recheck which convex hulls are within range //(e.g. position or range of the lightsource has changed) public bool NeedsHullCheck = true; //do we need to recalculate the vertices of the light volume @@ -278,7 +278,7 @@ namespace Barotrauma.Lights translateVertices = position - prevCalculatedPosition; return; } - + NeedsHullCheck = true; NeedsRecalculation = true; } @@ -360,7 +360,7 @@ namespace Barotrauma.Lights get; private set; } - + public float Range { get { return lightSourceParams.Range; } @@ -369,13 +369,29 @@ namespace Barotrauma.Lights lightSourceParams.Range = value; if (Math.Abs(prevCalculatedRange - lightSourceParams.Range) < 10.0f) return; - + NeedsHullCheck = true; NeedsRecalculation = true; prevCalculatedRange = lightSourceParams.Range; } } + private Vector2 lightTextureTargetSize; + + public Vector2 LightTextureTargetSize + { + get => lightTextureTargetSize; + set + { + NeedsRecalculation = true; + NeedsHullCheck = true; + lightTextureTargetSize = value; + } + } + + public Vector2 LightTextureOffset { get; set; } + public Vector2 LightTextureScale { get; set; } = Vector2.One; + public float TextureRange { get @@ -386,7 +402,7 @@ namespace Barotrauma.Lights /// /// Background lights are drawn behind submarines and they don't cast shadows. - /// + /// public bool IsBackground { get; @@ -462,7 +478,7 @@ namespace Barotrauma.Lights this.ParentSub = submarine; this.position = position; lightSourceParams = new LightSourceParams(range, color); - CastShadows = true; + CastShadows = true; texture = LightTexture; diffToSub = new Dictionary(); if (addLight) { GameMain.LightManager.AddLight(this); } @@ -494,7 +510,7 @@ namespace Barotrauma.Lights } CurrentBrightness = brightness; } - + /// /// Update the contents of ConvexHullList and check if we need to recalculate vertices /// @@ -509,7 +525,7 @@ namespace Barotrauma.Lights } /// - /// Recheck which convex hulls are in range (if needed), + /// Recheck which convex hulls are in range (if needed), /// and check if we need to recalculate vertices due to changes in the convex hulls /// private void CheckHullsInRange() @@ -561,20 +577,20 @@ namespace Barotrauma.Lights chList.List.Clear(); continue; } - + RefreshConvexHullList(chList, lightPos, sub); } } - else + else { //light is inside, convexhull outside if (sub == null) continue; - + //light and convexhull are both inside the same sub if (sub == ParentSub) { if (NeedsHullCheck) - { + { RefreshConvexHullList(chList, lightPos, sub); } } @@ -582,7 +598,7 @@ namespace Barotrauma.Lights else { if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) continue; - + lightPos -= (sub.Position - ParentSub.Position); Rectangle subBorders = sub.Borders; @@ -642,7 +658,7 @@ namespace Barotrauma.Lights foreach (ConvexHull hull in hulls) { hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); + hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); } //add a square-shaped boundary to make sure we've got something to construct the triangles from @@ -829,13 +845,13 @@ namespace Barotrauma.Lights if (intersection2.index < 0) return null; Segment seg1 = visibleSegments[intersection1.index]; Segment seg2 = visibleSegments[intersection2.index]; - + bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f; bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f; if (isPoint1 && isPoint2) { - //hit at the current segmentpoint -> place the segmentpoint into the list + //hit at the current segmentpoint -> place the segmentpoint into the list output.Add(p.WorldPos); foreach (ConvexHullList hullList in hullsInRange) @@ -938,7 +954,7 @@ namespace Barotrauma.Lights segment = i; } } - + return (segment, closestIntersection == null ? rayEnd : (Vector2)closestIntersection); } @@ -968,8 +984,8 @@ namespace Barotrauma.Lights overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); Vector2 origin = OverrideLightTextureOrigin; - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) - { + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; cosAngle = -cosAngle; sinAngle = -sinAngle; @@ -981,7 +997,7 @@ namespace Barotrauma.Lights // Add a vertex for the center of the mesh vertices[0] = new VertexPositionColorTexture(new Vector3(position.X, position.Y, 0), Color.White, GetUV(new Vector2(0.5f, 0.5f) + uvOffset, LightSpriteEffect)); - + //hacky fix to exc excessively large light volumes (they used to be up to 4x the range of the light if there was nothing to block the rays). //might want to tweak the raycast logic in a way that this isn't necessary /*float boundRadius = Range * 1.1f / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y))); @@ -999,7 +1015,7 @@ namespace Barotrauma.Lights for (int i = 0; i < rayCastHits.Count; i++) { Vector2 vertex = rayCastHits[i]; - + //we'll use the previous and next vertices to calculate the normals //of the two segments this vertex belongs to //so we can add new vertices based on these normals @@ -1007,7 +1023,7 @@ namespace Barotrauma.Lights Vector2 nextVertex = rayCastHits[i < rayCastHits.Count - 1 ? i + 1 : 0]; Vector2 rawDiff = vertex - drawPos; - + //calculate normal of first segment Vector2 nDiff1 = vertex - nextVertex; float tx = nDiff1.X; nDiff1.X = -nDiff1.Y; nDiff1.Y = tx; @@ -1015,7 +1031,7 @@ namespace Barotrauma.Lights //if the normal is pointing towards the light origin //rather than away from it, invert it if (Vector2.DistanceSquared(nDiff1, rawDiff) > Vector2.DistanceSquared(-nDiff1, rawDiff)) nDiff1 = -nDiff1; - + //calculate normal of second segment Vector2 nDiff2 = prevVertex - vertex; tx = nDiff2.X; nDiff2.X = -nDiff2.Y; nDiff2.Y = tx; @@ -1112,13 +1128,13 @@ namespace Barotrauma.Lights static Vector2 GetUV(Vector2 vert, SpriteEffects effects) { - if (effects == SpriteEffects.FlipHorizontally) - { - vert.X = 1.0f - vert.X; + if (effects == SpriteEffects.FlipHorizontally) + { + vert.X = 1.0f - vert.X; } - else if (effects == SpriteEffects.FlipVertically) - { - vert.Y = 1.0f - vert.Y; + else if (effects == SpriteEffects.FlipVertically) + { + vert.Y = 1.0f - vert.Y; } else if (effects == (SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically)) { @@ -1228,10 +1244,19 @@ namespace Barotrauma.Lights } drawPos.Y = -drawPos.Y; - LightSprite.Draw( - spriteBatch, drawPos, - new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness), - origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, LightSpriteEffect); + Color color = new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness); + + if (LightTextureTargetSize != Vector2.Zero) + { + LightSprite.DrawTiled(spriteBatch, drawPos, LightTextureTargetSize, color, startOffset: LightTextureOffset, textureScale: LightTextureScale); + } + else + { + LightSprite.Draw( + spriteBatch, drawPos, + color, + origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, LightSpriteEffect); + } } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) @@ -1255,7 +1280,7 @@ namespace Barotrauma.Lights GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color); GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color); } - } + } } public void CheckConditionals() @@ -1280,10 +1305,10 @@ namespace Barotrauma.Lights if (!CastShadows) { Texture2D currentTexture = texture ?? LightTexture; - if (OverrideLightTexture != null) { currentTexture = OverrideLightTexture.Texture; } + if (OverrideLightTexture != null) { currentTexture = OverrideLightTexture.Texture; } - Vector2 center = OverrideLightTexture == null ? - new Vector2(currentTexture.Width / 2, currentTexture.Height / 2) : + Vector2 center = OverrideLightTexture == null ? + new Vector2(currentTexture.Width / 2, currentTexture.Height / 2) : OverrideLightTexture.Origin; float scale = Range / (currentTexture.Width / 2.0f); @@ -1291,7 +1316,7 @@ namespace Barotrauma.Lights if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } drawPos.Y = -drawPos.Y; - spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation, center, scale, SpriteEffects.None, 1); + spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation + MathHelper.ToRadians(LightSourceParams.Rotation), center, scale, SpriteEffects.None, 1); return; } @@ -1317,8 +1342,8 @@ namespace Barotrauma.Lights Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; lightEffect.World = - Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * - Matrix.CreateRotationZ(rotateVertices) * + Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * + Matrix.CreateRotationZ(rotateVertices - MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * transform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 7a29743ad..91eb1ce2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -265,7 +265,7 @@ namespace Barotrauma if (!currentDisplayLocation.Discovered) { RemoveFogOfWar(currentDisplayLocation); - currentDisplayLocation.Discovered = true; + currentDisplayLocation.Discover(); if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = currentDisplayLocation; @@ -426,7 +426,7 @@ namespace Barotrauma Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); SelectLocation(-1); if (GameMain.Client == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 9fad6fc1c..642813f4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -6,6 +6,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Lights; namespace Barotrauma { @@ -17,7 +18,7 @@ namespace Barotrauma private static Vector2 startMovingPos = Vector2.Zero; private static float keyDelay; - + public static Vector2 StartMovingPos => startMovingPos; public event Action Resized; @@ -97,13 +98,13 @@ namespace Barotrauma /// public float GetDrawDepth(float baseDepth, Sprite sprite) { - float depth = baseDepth + float depth = baseDepth //take texture into account to get entities with (roughly) the same base depth and texture to render consecutively to minimize texture swaps + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.00001f + ID % 100 * 0.000001f; return Math.Min(depth, 1.0f); } - + /// /// Update the selection logic in submarine editor /// @@ -218,7 +219,7 @@ namespace Barotrauma } } } - } + } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); MapEntity highLightedEntity = null; @@ -284,13 +285,13 @@ namespace Barotrauma //mouse released -> move the entities to the new position of the mouse Vector2 moveAmount = position - startMovingPos; - + if (!isShiftDown) { moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; } - + if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) { if (!isShiftDown) { moveAmount = Submarine.VectorToWorldGrid(moveAmount); } @@ -321,10 +322,10 @@ namespace Barotrauma else { SoundPlayer.PlayUISound(GUISoundType.PickItemFail); - } + } } } - + SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList),SelectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); if (deposited.Any() && deposited.Any(entity => entity is Item)) { @@ -423,20 +424,24 @@ namespace Barotrauma //select wire if both items it's connected to are selected var selectedItems = SelectedList.Where(e => e is Item).Cast().ToList(); - foreach (Item item in selectedItems) + foreach (Item item in Item.ItemList) { - if (item.Connections == null) continue; - foreach (Connection c in item.Connections) - { - foreach (Wire w in c.Wires) - { - if (w == null || SelectedList.Contains(w.Item)) continue; + var wire = item.GetComponent(); + if (wire == null) { continue; } + Item item0 = wire.Connections[0]?.Item; + Item item1 = wire.Connections[1]?.Item; - if (w.OtherConnection(c) != null && SelectedList.Contains(w.OtherConnection(c).Item)) - { - SelectedList.Add(w.Item); - } - } + if (item0 == null && item1 != null) + { + item0 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + } + else if (item0 != null && item1 == null) + { + item1 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + } + if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1)) + { + SelectedList.Add(item); } } @@ -453,8 +458,8 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.KeyUp(Keys.Space) && - PlayerInput.KeyUp(Keys.LeftAlt) && - PlayerInput.KeyUp(Keys.RightAlt) && + PlayerInput.KeyUp(Keys.LeftAlt) && + PlayerInput.KeyUp(Keys.RightAlt) && (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn)))) { //if clicking a selected entity, start moving it @@ -482,7 +487,7 @@ namespace Barotrauma int xKeysDown = (left + right); int yKeysDown = (up + down); - + if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; } @@ -512,7 +517,7 @@ namespace Barotrauma bool isShiftDown = PlayerInput.IsShiftDown(); if (!isShiftDown) return null; - + foreach (MapEntity e in mapEntityList) { if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } @@ -662,7 +667,7 @@ namespace Barotrauma { if (SelectedList.Contains(entity)) { return; } SelectedList.Add(entity); - HandleDoorGapLinks(entity, + HandleDoorGapLinks(entity, onGapFound: (door, gap) => { door.RefreshLinkedGap(); @@ -670,8 +675,8 @@ namespace Barotrauma { SelectedList.Add(gap); } - }, - onDoorFound: (door, gap) => + }, + onDoorFound: (door, gap) => { if (!SelectedList.Contains(door.Item)) { @@ -715,7 +720,7 @@ namespace Barotrauma onGapFound: (door, gap) => SelectedList.Remove(gap), onDoorFound: (door, gap) => SelectedList.Remove(door.Item)); } - + static partial void UpdateAllProjSpecific(float deltaTime) { var entitiesToRender = Submarine.VisibleEntities ?? mapEntityList; @@ -748,7 +753,7 @@ namespace Barotrauma moveAmount.Y = -moveAmount.Y; bool isShiftDown = PlayerInput.IsShiftDown(); - + if (!isShiftDown) { moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; @@ -761,21 +766,21 @@ namespace Barotrauma foreach (MapEntity e in SelectedList) { SpriteEffects spriteEffects = SpriteEffects.None; - switch (e) + switch (e) { - case Item item: + case Item item: { if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; break; } - case Structure structure: + case Structure structure: { if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; break; } - case WayPoint wayPoint: + case WayPoint wayPoint: { Vector2 drawPos = e.WorldPosition; drawPos.Y = -drawPos.Y; @@ -812,7 +817,7 @@ namespace Barotrauma posY = -posY; - Vector2[] corners = + Vector2[] corners = { new Vector2(posX, posY), new Vector2(posX + sizeX, posY), @@ -878,7 +883,7 @@ namespace Barotrauma { MapEntity firstSelected = SelectedList.First(); - float minX = firstSelected.WorldRect.X, + float minX = firstSelected.WorldRect.X, maxX = firstSelected.WorldRect.Right; foreach (MapEntity entity in SelectedList) @@ -903,7 +908,7 @@ namespace Barotrauma foreach (MapEntity entity in SelectedList) { - + minY = Math.Min(minY, entity.WorldRect.Y - entity.WorldRect.Height); maxY = Math.Max(maxY, entity.WorldRect.Y); } @@ -943,21 +948,21 @@ namespace Barotrauma } /// - /// Copy the selected entities to the "clipboard" (copiedList) + /// Copy the selected entities to the "clipboard" (copiedList) /// public static void Copy(List entities) { if (entities.Count == 0) { return; } CopyEntities(entities); } - + /// /// Copy the entities to the "clipboard" (copiedList) and delete them /// public static void Cut(List entities) { if (entities.Count == 0) { return; } - + CopyEntities(entities); SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(entities), true)); @@ -989,6 +994,10 @@ namespace Barotrauma clone.Move(moveAmount); clone.Submarine = Submarine.MainSub; } + foreach (MapEntity clone in SelectedList) + { + (clone as Item)?.GetComponent()?.SetContainedItemPositions(); + } SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false)); } @@ -1012,9 +1021,9 @@ namespace Barotrauma return newEntities; } - public virtual void AddToGUIUpdateList() + public virtual void AddToGUIUpdateList(int order = 0) { - if (editingHUD != null && editingHUD.UserData == this) editingHUD.AddToGUIUpdateList(); + if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(order: order); } } public virtual void UpdateEditing(Camera cam, float deltaTime) { } @@ -1049,7 +1058,7 @@ namespace Barotrauma editingHUD.RectTransform.Resize( new Point( - editingHUD.RectTransform.NonScaledSize.X, + editingHUD.RectTransform.NonScaledSize.X, MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false); listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false); } @@ -1089,7 +1098,7 @@ namespace Barotrauma { prevRect = new Rectangle(Rect.Location, Rect.Size); } - + Vector2 placePosition = new Vector2(rect.X, rect.Y); Vector2 placeSize = new Vector2(rect.Width, rect.Height); @@ -1140,6 +1149,15 @@ namespace Barotrauma var oldData = new List { prevRect.Value }; SubEditorScreen.StoreCommand(new TransformCommand(new List { this }, newData, oldData, true)); } + + if (this is Structure structure) + { + foreach (LightSource light in structure.Lights) + { + light.LightTextureTargetSize = Rect.Size.ToVector2(); + light.Position = rect.Location.ToVector2(); + } + } prevRect = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 219b290af..b3b2a653f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -14,12 +14,14 @@ namespace Barotrauma { partial class Structure : MapEntity, IDamageable, IServerSerializable { - public static bool ShowWalls = true, ShowStructures = true; + public static bool ShowWalls = true, ShowStructures = true; private List convexHulls; private readonly Dictionary spriteAnimState = new Dictionary(); + public readonly List Lights = new List(); + public override bool SelectableInEditor { get @@ -41,7 +43,7 @@ namespace Barotrauma { get; set; - } + } partial void InitProjSpecific() { @@ -88,7 +90,23 @@ namespace Barotrauma if (editingHUD == null || editingHUD.UserData as Structure != this) { editingHUD = CreateEditingHUD(Screen.Selected != GameMain.SubEditorScreen); - } + } + } + + private void SetLightTextureOffset() + { + Vector2 textOffset = textureOffset; + if (FlippedX) { textOffset.X = -textOffset.X; } + if (FlippedY) { textOffset.Y = -textOffset.Y; } + + foreach (LightSource light in Lights) + { + Vector2 bgOffset = new Vector2( + MathUtils.PositiveModulo((int)-textOffset.X, light.texture.Width), + MathUtils.PositiveModulo((int)-textOffset.Y, light.texture.Height)); + + light.LightTextureOffset = bgOffset; + } } public GUIComponent CreateEditingHUD(bool inGame = false) @@ -175,12 +193,12 @@ namespace Barotrauma buttonContainer.RectTransform.IsFixedSize = true; GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(b => ((GUIButton)b).TextBlock)); editor.AddCustomContent(buttonContainer, editor.ContentCount); - + PositionEditingHUD(); return editingHUD; } - + partial void OnImpactProjSpecific(Fixture f1, Fixture f2, Contact contact) { if (!Prefab.Platform && Prefab.StairDirection == Direction.None) @@ -261,19 +279,21 @@ namespace Barotrauma else if (HiddenInGame) { return; } Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted ? GUI.Style.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; - + if (IsSelected && editing) { //color = Color.Lerp(color, Color.Gold, 0.5f); color = spriteColor; + + Vector2 rectSize = rect.Size.ToVector2(); if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } Vector2 bodyPos = WorldPosition + BodyOffset; - GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, + GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } @@ -305,14 +325,14 @@ namespace Barotrauma } else { - dropShadowOffset = IsHorizontal ? - new Vector2(0.0f, Math.Sign(Submarine.HiddenSubPosition.Y - Position.Y) * 10.0f) : + dropShadowOffset = IsHorizontal ? + new Vector2(0.0f, Math.Sign(Submarine.HiddenSubPosition.Y - Position.Y) * 10.0f) : new Vector2(Math.Sign(Submarine.HiddenSubPosition.X - Position.X) * 10.0f, 0.0f); } } dropShadowOffset.Y = -dropShadowOffset.Y; } - + SpriteEffects oldEffects = Prefab.BackgroundSprite.effects; Prefab.BackgroundSprite.effects ^= SpriteEffects; @@ -372,13 +392,13 @@ namespace Barotrauma if (!HasDamage && i == 0) { drawSection = new Rectangle( - drawSection.X, - drawSection.Y, + drawSection.X, + drawSection.Y, Sections[Sections.Length -1 ].rect.Right - drawSection.X, drawSection.Y - (Sections[Sections.Length - 1].rect.Y - Sections[Sections.Length - 1].rect.Height)); i = Sections.Length; } - + Vector2 sectionOffset = new Vector2( Math.Abs(rect.Location.X - drawSection.Location.X), Math.Abs(rect.Location.Y - drawSection.Location.Y)); @@ -496,7 +516,7 @@ namespace Barotrauma } else { - if (!conditional.Matches(this)) { return false; } + if (!conditional.Matches(this)) { return false; } } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 28e2bbdb2..6d4db8e52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -177,7 +178,6 @@ namespace Barotrauma //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); - private static readonly HashSet visibleRuins = new HashSet(); public static void CullEntities(Camera cam) { visibleSubs.Clear(); @@ -197,24 +197,6 @@ namespace Barotrauma } } - visibleRuins.Clear(); - if (Level.Loaded != null) - { - foreach (Ruin ruin in Level.Loaded.Ruins) - { - Rectangle worldBorders = new Rectangle( - ruin.Area.X - 500, - ruin.Area.Y + ruin.Area.Height + 500, - ruin.Area.Width + 1000, - ruin.Area.Height + 1000); - - if (RectsOverlap(worldBorders, cam.WorldView)) - { - visibleRuins.Add(ruin); - } - } - } - if (visibleEntities == null) { visibleEntities = new List(MapEntity.mapEntityList.Count); @@ -231,10 +213,6 @@ namespace Barotrauma { if (!visibleSubs.Contains(entity.Submarine)) { continue; } } - else if (entity.ParentRuin != null) - { - if (!visibleRuins.Contains(entity.ParentRuin)) { continue; } - } if (entity.IsVisible(worldView)) { visibleEntities.Add(entity); } } @@ -325,7 +303,7 @@ namespace Barotrauma depthSortedDamageable.Insert(i, structure); } } - + foreach (Structure s in depthSortedDamageable) { s.DrawDamage(spriteBatch, damageEffect, editing); @@ -403,6 +381,8 @@ namespace Barotrauma } } + // TODO remove + [Obsolete("Use MiniMap.CreateMiniMap()")] public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null, bool ignoreOutpost = false) { Rectangle worldBorders = GetDockedBorders(); @@ -415,26 +395,124 @@ namespace Barotrauma float scale = 0.9f; GUIFrame hullContainer = new GUIFrame(new RectTransform( - (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale, - parent.RectTransform, Anchor.Center), - style: null); + (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale, + parent.RectTransform, Anchor.Center), + style: null) + { + UserData = "hullcontainer" + }; var connectedSubs = GetConnectedSubs(); - foreach (Hull hull in Hull.hullList) - { - if (hull.Submarine != this && !connectedSubs.Contains(hull.Submarine)) { continue; } - if (ignoreOutpost && !IsEntityFoundOnThisSub(hull, true)) { continue; } + HashSet hullList = Hull.hullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); + + Dictionary> combinedHulls = new Dictionary>(); + + foreach (Hull hull in hullList) + { + if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; } + + List linkedHulls = new List(); + MiniMap.GetLinkedHulls(hull, linkedHulls); + + linkedHulls.Remove(hull); + + foreach (Hull linkedHull in linkedHulls) + { + if (!combinedHulls.ContainsKey(hull)) + { + combinedHulls.Add(hull, new HashSet()); + } + + combinedHulls[hull].Add(linkedHull); + } + } + + foreach (Hull hull in hullList) + { Vector2 relativeHullPos = new Vector2( - (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, + (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, (worldBorders.Y - hull.WorldRect.Y) / (float)worldBorders.Height); Vector2 relativeHullSize = new Vector2(hull.Rect.Width / (float)worldBorders.Width, hull.Rect.Height / (float)worldBorders.Height); - var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: Color.DarkCyan * 0.8f) + bool hideHull = combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull)); + + if (hideHull) { continue; } + + Color color = Color.DarkCyan * 0.8f; + + var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: color) { UserData = hull }; - new GUIFrame(new RectTransform(Vector2.One, hullFrame.RectTransform), style: "ScanLines", color: Color.DarkCyan * 0.8f); + + new GUIFrame(new RectTransform(Vector2.One, hullFrame.RectTransform), style: "ScanLines", color: color); + } + + foreach (var (mainHull, linkedHulls) in combinedHulls) + { + MiniMapHullData data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders); + + Vector2 relativeHullPos = new Vector2( + (data.Bounds.X - worldBorders.X) / worldBorders.Width, + (worldBorders.Y - data.Bounds.Y) / worldBorders.Height); + + Vector2 relativeHullSize = new Vector2(data.Bounds.Width / worldBorders.Width, data.Bounds.Height / worldBorders.Height); + + Color color = Color.DarkCyan * 0.8f; + + float highestY = 0f, + highestX = 0f; + + foreach (var (r, _) in data.RectDatas) + { + float y = r.Y - -r.Height, + x = r.X; + + if (y > highestY) { highestY = y; } + if (x > highestX) { highestX = x; } + } + + HashSet frames = new HashSet(); + + foreach (var (snappredRect, hull) in data.RectDatas) + { + RectangleF rect = snappredRect; + rect.Height = -rect.Height; + rect.Y -= rect.Height; + + var (parentW, parentH) = hullContainer.Rect.Size.ToVector2(); + Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH); + // TODO this won't be required if we some day switch RectTransform to use RectangleF + Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH); + + GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: color) + { + UserData = hull, + UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y) + }; + + frames.Add(hullFrame); + } + + new GUICustomComponent(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, (spriteBatch, component) => + { + foreach (List list in data.Polygon) + { + spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.Color, 2f); + } + }, (deltaTime, component) => + { + if (component.Parent.Rect.Size != data.ParentSize) + { + data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders); + } + }) + { + UserData = frames, + Color = color, + CanBeFocused = false + }; } if (pointsOfInterest != null) @@ -453,6 +531,64 @@ namespace Barotrauma } } + public static MiniMapHullData ConstructLinkedHulls(Hull mainHull, HashSet linkedHulls, GUIComponent parent, Rectangle worldBorders) + { + Rectangle parentRect = parent.Rect; + + Dictionary rects = new Dictionary(); + Rectangle worldRect = mainHull.WorldRect; + worldRect.Y = -worldRect.Y; + + rects.Add(mainHull, worldRect); + + foreach (Hull hull in linkedHulls) + { + Rectangle rect = hull.WorldRect; + rect.Y = -rect.Y; + + worldRect = Rectangle.Union(worldRect, rect); + rects.Add(hull, rect); + } + + worldRect.Y = -worldRect.Y; + + List normalizedRects = new List(); + List hullRefs = new List(); + foreach (var (hull, rect) in rects) + { + Rectangle wRect = rect; + wRect.Y = -wRect.Y; + + var (posX, posY) = new Vector2( + (wRect.X - worldBorders.X) / (float)worldBorders.Width, + (worldBorders.Y - wRect.Y) / (float)worldBorders.Height); + + var (scaleX, scaleY) = new Vector2(wRect.Width / (float)worldBorders.Width, wRect.Height / (float)worldBorders.Height); + + RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, scaleX * parentRect.Width, scaleY * parentRect.Height); + + normalizedRects.Add(newRect); + hullRefs.Add(hull); + } + + ImmutableArray snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1); + + List> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles); + + List> scaledPolygon = new List>(); + + foreach (List list in polygon) + { + var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list); + float sizeX = polySizeX - 1f, + sizeY = polySizeY - 1f; + + scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY))); + } + + return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray()); + } + public void CheckForErrors() { List errorMsgs = new List(); @@ -467,7 +603,7 @@ namespace Barotrauma } } - if (Info.Type != SubmarineType.OutpostModule || + if (Info.Type != SubmarineType.OutpostModule || (Info.OutpostModuleInfo?.ModuleFlags.Any(f => !f.Equals("hallwayvertical", StringComparison.OrdinalIgnoreCase) && !f.Equals("hallwayhorizontal", StringComparison.OrdinalIgnoreCase)) ?? true)) { if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) @@ -535,7 +671,7 @@ namespace Barotrauma int wireCount = item.Connections[i].Wires.Count(w => w != null); if (doorLinks + wireCount > item.Connections[i].MaxWires) { - errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", + errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", new string[] { "[doorcount]", "[freeconnectioncount]" }, new string[] { doorLinks.ToString(), (item.Connections[i].MaxWires - wireCount).ToString() })); break; @@ -632,7 +768,7 @@ namespace Barotrauma return SubEditorScreen.SuppressedWarnings.Contains(type); } } - + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (type != ServerNetObject.ENTITY_POSITION) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 68e65bc1f..5083fe7c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -600,6 +600,8 @@ namespace Barotrauma var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + var prevRasterizerState = GameMain.Instance.GraphicsDevice.RasterizerState; + GameMain.Instance.GraphicsDevice.RasterizerState = GameMain.ScissorTestEnable; spriteRecorder.Render(camera); @@ -643,6 +645,7 @@ namespace Barotrauma spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + GameMain.Instance.GraphicsDevice.RasterizerState = prevRasterizerState; spriteBatch.Begin(SpriteSortMode.Deferred); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 85da43a34..0696a24a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -127,6 +127,13 @@ namespace Barotrauma ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), color); + if (Tunnel?.Type != null) + { + GUI.SmallFont.DrawString(spriteBatch, + Tunnel.Type.ToString(), + new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45), + color); + } } public override bool IsMouseOn(Vector2 position) @@ -164,10 +171,7 @@ namespace Barotrauma { foreach (MapEntity e in mapEntityList) { - if (e.GetType() != typeof(WayPoint)) continue; - if (e == this) continue; - - if (!Submarine.RectContains(e.Rect, position)) continue; + if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } if (linkedTo.Contains(e)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index feee46eb0..67c758aa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -64,7 +64,10 @@ namespace Barotrauma.Networking string orderOption = orderMessageInfo.OrderOption; orderOption ??= orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] : ""; - txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, orderOption: orderOption); + txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, + givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, + orderOption: orderOption, + priority: orderMessageInfo.Priority); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 77c4765eb..e919460fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -552,6 +552,13 @@ namespace Barotrauma.Networking okButton.OnClicked += msgBox.Close; var cancelButton = msgBox.Buttons[1]; cancelButton.OnClicked += msgBox.Close; + passwordBox.OnEnterPressed += (GUITextBox textBox, string text) => + { + msgBox.Close(); + clientPeer?.SendPassword(passwordBox.Text); + requiresPw = false; + return true; + }; okButton.OnClicked += (GUIButton button, object obj) => { @@ -567,6 +574,8 @@ namespace Barotrauma.Networking GameMain.ServerListScreen.Select(); return true; }; + yield return CoroutineStatus.Running; + passwordBox.Select(); while (GUIMessageBox.MessageBoxes.Contains(msgBox)) { @@ -1461,6 +1470,7 @@ namespace Barotrauma.Networking bool respawnAllowed = inc.ReadBoolean(); serverSettings.AllowDisguises = inc.ReadBoolean(); serverSettings.AllowRewiring = inc.ReadBoolean(); + serverSettings.AllowFriendlyFire = inc.ReadBoolean(); serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); @@ -2753,6 +2763,9 @@ namespace Barotrauma.Networking msg.Write((byte)characterInfo.BeardIndex); msg.Write((byte)characterInfo.MoustacheIndex); msg.Write((byte)characterInfo.FaceAttachmentIndex); + msg.WriteColorR8G8B8(characterInfo.SkinColor); + msg.WriteColorR8G8B8(characterInfo.HairColor); + msg.WriteColorR8G8B8(characterInfo.FacialHairColor); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; int count = Math.Min(jobPreferences.Count, 3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 8f08b8966..911d2eb23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -288,7 +288,7 @@ namespace Barotrauma.Networking heartbeatTimer = 5.0; #if DEBUG - CoroutineManager.InvokeAfter(() => + CoroutineManager.Invoke(() => { if (GameMain.Client == null) { return; } if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && sendType != Steamworks.P2PSend.Reliable) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 931cd5d44..ee2ffa7c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -110,12 +110,7 @@ namespace Barotrauma.Networking if (frame == null) { return; } - var previewContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), frame.RectTransform, Anchor.Center)) - { - Stretch = true - }; - - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform, Anchor.CenterLeft), ServerName, font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUI.LargeFont) { ToolTip = ServerName }; @@ -141,41 +136,30 @@ namespace Barotrauma.Networking } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); - bool hidePlaystyleBanner = previewContainer.Rect.Height < 380 || !PlayStyle.HasValue; + bool hidePlaystyleBanner = !PlayStyle.HasValue; if (!hidePlaystyleBanner) { PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height; - var playStyleBanner = new GUIImage(new RectTransform(new Point(previewContainer.Rect.Width, (int)(previewContainer.Rect.Width / playStyleBannerAspectRatio)), previewContainer.RectTransform), + var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform), playStyleBannerSprite, null, true); - var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, + var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.06f) }, TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, font: GUI.SmallFont, textAlignment: Alignment.Center, color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; - - var serverTypeContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.2f), playStyleBanner.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), - "MainMenuNotifBackground", Color.Black) - { - CanBeFocused = false, - }; - - var serverType = new GUITextBlock(new RectTransform(Vector2.One, serverTypeContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); - } - else - { - var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), previewContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.CenterLeft); } + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), + TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.TopLeft); + serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), previewContainer.RectTransform)) + var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) { Stretch = true }; @@ -285,7 +269,6 @@ namespace Barotrauma.Networking else usingWhiteList.Selected = UsingWhiteList.Value; - content.RectTransform.SizeChanged += () => { GUITextBlock.AutoScaleAndNormalize(allowSpectating.TextBlock, allowRespawn.TextBlock, usingWhiteList.TextBlock); @@ -294,7 +277,7 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUI.SubHeadingFont); - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform)) { ScrollBarVisible = true }; + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) { ScrollBarVisible = true }; if (ContentPackageNames.Count == 0) { new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) @@ -309,7 +292,7 @@ namespace Barotrauma.Networking var packageText = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, ContentPackageNames[i]) { - Enabled = false + CanBeFocused = false }; if (i < ContentPackageHashes.Count) { @@ -322,7 +305,7 @@ namespace Barotrauma.Networking //workshop download link found if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - packageText.TextColor = Color.Yellow; + packageText.TextColor = GUI.Style.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } else //no package or workshop download link found, tough luck diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index d9647ac72..13604817e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -1207,7 +1207,7 @@ namespace Barotrauma.Steam foreach (string file in allPackageFiles) { if (file == metaDataFilePath) { continue; } - string relativePath = UpdaterUtil.GetRelativePath(file, item.Directory); + string relativePath = Path.GetRelativePath(item.Directory, file); string fullPath = Path.GetFullPath(relativePath); if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; } nonContentFiles.Add(relativePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 3a4852708..6b7cd2e8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Reflection.Metadata; namespace Barotrauma.Particles { @@ -16,6 +15,8 @@ namespace Barotrauma.Particles public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull); public OnChangeHullHandler OnChangeHull; + public OnChangeHullHandler OnCollision; + private Vector2 position; private Vector2 prevPosition; @@ -166,6 +167,7 @@ namespace Barotrauma.Particles HighQualityCollisionDetection = false; OnChangeHull = null; + OnCollision = null; subEmitters.Clear(); hasSubEmitters = false; @@ -340,12 +342,20 @@ namespace Barotrauma.Particles Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } + if (prefab.DeleteOnCollision) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } + if (prefab.DeleteOnCollision) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } collisionNormal = new Vector2(0.0f, -1.0f); } @@ -368,6 +378,7 @@ namespace Barotrauma.Particles handleCollision(gapFound, collisionNormal); } + collisionNormal = Vector2.Zero; if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) { if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } @@ -487,6 +498,8 @@ namespace Barotrauma.Particles velocity.Y = Math.Sign(collisionNormal.Y) * Math.Abs(velocity.Y) * prefab.Restitution; } + OnCollision?.Invoke(position, currentHull); + velocity += subVel; } @@ -523,6 +536,8 @@ namespace Barotrauma.Particles velocity.Y *= (1.0f - prefab.Friction); } + OnCollision?.Invoke(position, currentHull); + velocity *= prefab.Restitution; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f54793630..72d20579a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -138,6 +138,8 @@ namespace Barotrauma.Particles public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) { + if (GameMain.Client?.MidRoundSyncing ?? false) { return; } + if (initialDelay < Prefab.Properties.InitialDelay) { initialDelay += deltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 9ce02308e..1b5c3fdc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -133,22 +133,41 @@ namespace Barotrauma.Particles public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { - if (particleCount >= MaxParticles || prefab == null || prefab.Sprites.Count == 0) { return null; } - - // this should be optimized for tracers after prototyping - if (tracerPoints == null) + if (prefab == null || prefab.Sprites.Count == 0) { return null; } + + if (particleCount >= MaxParticles) { - Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); - - Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); - Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); - - Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + for (int i = 0; i < particleCount; i++) + { + if (particles[i].Prefab.Priority < prefab.Priority) + { + RemoveParticle(i); + break; + } + } + if (particleCount >= MaxParticles) { return null; } } + Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); + + Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); + Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); + + if (tracerPoints != null) + { + minPos = new Vector2( + Math.Min(Math.Min(minPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), + Math.Min(Math.Min(minPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); + maxPos = new Vector2( + Math.Max(Math.Max(maxPos.X, tracerPoints.Item1.X), tracerPoints.Item2.X), + Math.Max(Math.Max(maxPos.Y, tracerPoints.Item1.Y), tracerPoints.Item2.Y)); + } + + Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); + + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + if (particles[particleCount] == null) particles[particleCount] = new Particle(); particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); @@ -165,7 +184,7 @@ namespace Barotrauma.Particles public ParticlePrefab FindPrefab(string prefabName) { - return Prefabs.Find(p => p.Identifier == prefabName); + return Prefabs.Find(p => p.Identifier.Equals(prefabName, StringComparison.OrdinalIgnoreCase)); } private void RemoveParticle(int index) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 104789a59..9882048e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -180,16 +180,13 @@ namespace Barotrauma.Particles [Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } - [Editable] - [Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMin { get; private set; } - [Editable] - [Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable] - [Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable, Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- @@ -215,6 +212,9 @@ namespace Barotrauma.Particles [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } + [Editable, Serialize(0, false, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] + public int Priority { get; private set; } + //animation ----------------------------------------- [Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 0ee887f01..b8beb4aa6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -54,9 +54,11 @@ namespace Barotrauma executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); SteamManager.Initialize(); + EnableNvOptimus(); Game = new GameMain(args); Game.Run(); Game.Dispose(); + FreeNvOptimus(); CrossThread.ProcessTasks(); } @@ -263,6 +265,27 @@ namespace Barotrauma " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/", filePath); } } - } + + private static IntPtr nvApi64Dll = IntPtr.Zero; + private static void EnableNvOptimus() + { +#if WINDOWS && X64 + // We force load nvapi64.dll so nvidia gives us the dedicated GPU on optimus laptops. + // This is not a method for getting optimus that is documented by nvidia, but it works, so... + if (NativeLibrary.TryLoad("nvapi64.dll", out nvApi64Dll)) + { + DebugConsole.Log("Loaded nvapi64.dll successfully"); + } #endif } + + private static void FreeNvOptimus() + { + #warning TODO: determine if we can do this safely + //NativeLibrary.Free(nvApi64Dll); + } + + } +#endif + + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs new file mode 100644 index 000000000..81ead93e1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + abstract class CampaignSetupUI + { + protected readonly GUIComponent newGameContainer, loadGameContainer; + + protected GUIListBox subList; + protected GUIListBox saveList; + protected List subTickBoxes; + + protected GUITextBox saveNameBox, seedBox; + + protected GUILayoutGroup subPreviewContainer; + + protected GUIButton loadGameButton; + + public Action StartNewGame; + public Action LoadGame; + + protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + protected CategoryFilter subFilter = CategoryFilter.All; + + public GUIButton StartButton + { + get; + protected set; + } + + public GUITextBlock InitialMoneyText + { + get; + protected set; + } + + public GUITickBox EnableRadiationToggle { get; set; } + public GUILayoutGroup CampaignSettingsContent { get; set; } + + public GUIButton CampaignCustomizeButton { get; set; } + public GUIMessageBox CampaignCustomizeSettings { get; set; } + + public GUITextBlock MaxMissionCountText; + + public CampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) + { + this.newGameContainer = newGameContainer; + this.loadGameContainer = loadGameContainer; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs similarity index 51% rename from Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs rename to Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 7882cc77b..e02424ee4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -1,4 +1,4 @@ -using Barotrauma.Tutorials; +using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -10,58 +10,17 @@ using Barotrauma.Extensions; namespace Barotrauma { - class CampaignSetupUI + class MultiPlayerCampaignSetupUI : CampaignSetupUI { - private readonly GUIComponent newGameContainer, loadGameContainer; - - private GUIListBox subList; - private GUIListBox saveList; - private List subTickBoxes; - - private readonly GUITextBox saveNameBox, seedBox; - - private readonly GUILayoutGroup subPreviewContainer; - - private GUIButton loadGameButton, deleteMpSaveButton; + private GUIButton deleteMpSaveButton; - public Action StartNewGame; - public Action LoadGame; - - private enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; - private CategoryFilter subFilter = CategoryFilter.All; - - public GUIButton StartButton + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + : base(newGameContainer, loadGameContainer) { - get; - private set; - } - - public GUITextBlock InitialMoneyText - { - get; - private set; - } - - public GUITickBox EnableRadiationToggle { get; set; } - public GUILayoutGroup CampaignSettingsContent { get; set; } - - public GUIButton CampaignCustomizeButton { get; set; } - public GUIMessageBox CampaignCustomizeSettings { get; set; } - - public GUITextBlock MaxMissionCountText; - - private readonly bool isMultiplayer; - - public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) - { - this.isMultiplayer = isMultiplayer; - this.newGameContainer = newGameContainer; - this.loadGameContainer = loadGameContainer; - var columnContainer = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: true) { Stretch = true, - RelativeSpacing = isMultiplayer ? 0.0f : 0.02f + RelativeSpacing = 0.0f }; var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) @@ -70,7 +29,7 @@ namespace Barotrauma RelativeSpacing = 0.015f }; - var rightColumn = new GUILayoutGroup(new RectTransform(isMultiplayer ? Vector2.Zero : new Vector2(1.5f, 1.0f), columnContainer.RectTransform)) + var rightColumn = new GUILayoutGroup(new RectTransform(Vector2.Zero, columnContainer.RectTransform)) { Stretch = true, RelativeSpacing = 0.015f @@ -88,47 +47,11 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - if (!isMultiplayer) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + // Spacing to fix the multiplayer campaign setup layout + CreateMultiplayerCampaignSubList(leftColumn.RectTransform); - var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); - moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); - moddedDropdown.AddItem(TextManager.Get("servertag.modded.false"), CategoryFilter.Vanilla); - moddedDropdown.AddItem(TextManager.Get("customrank"), CategoryFilter.Custom); - moddedDropdown.Select(0); - - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) - { - Stretch = true - }; - - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - - moddedDropdown.OnSelected = (component, data) => - { - searchBox.Text = string.Empty; - subFilter = (CategoryFilter)data; - UpdateSubList(SubmarineInfo.SavedSubmarines); - return true; - }; - - subList.OnSelected = OnSubSelected; - } - else // Spacing to fix the multiplayer campaign setup layout - { - CreateMultiplayerCampaignSubList(leftColumn.RectTransform); - - //spacing - //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); - } + //spacing + //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); // New game right side subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)) @@ -137,8 +60,7 @@ namespace Barotrauma }; var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), - (isMultiplayer ? leftColumn : rightColumn).RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); - if (!isMultiplayer) { buttonContainer.IgnoreLayoutGroups = true; } + leftColumn.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) { @@ -152,16 +74,8 @@ namespace Barotrauma SubmarineInfo selectedSub = null; - if (!isMultiplayer) - { - if (!(subList.SelectedData is SubmarineInfo)) { return false; } - selectedSub = subList.SelectedData as SubmarineInfo; - } - else - { - if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } - selectedSub = GameMain.NetLobbyScreen.SelectedSub; - } + if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } + selectedSub = GameMain.NetLobbyScreen.SelectedSub; if (selectedSub.SubmarineClass == SubmarineClass.Undefined) { @@ -177,28 +91,14 @@ namespace Barotrauma return false; } - string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer, saveNameBox.Text); + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; CampaignSettings settings = new CampaignSettings(); - if (isMultiplayer) - { - settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); - settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); - } - else - { - settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) - { - settings.MaxMissionCount = missionCount; - } - else - { - settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; - } - } + settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); + settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { if (!hasRequiredContentPackages) @@ -213,10 +113,7 @@ namespace Barotrauma if (GUIMessageBox.MessageBoxes.Count == 0) { StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); } return true; }; @@ -233,10 +130,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (button, obj) => { StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; @@ -248,56 +142,27 @@ namespace Barotrauma else { StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); } return true; } }; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(isMultiplayer ? 0.6f : 0.3f, 1f), buttonContainer.RectTransform), "", font: isMultiplayer ? GUI.Style.SmallFont : GUI.Style.Font, textColor: GUI.Style.Green) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUI.Style.SmallFont, textColor: GUI.Style.Green) { TextGetter = () => { int initialMoney = CampaignMode.InitialMoney; - if (isMultiplayer) + if (GameMain.NetLobbyScreen.SelectedSub != null) { - if (GameMain.NetLobbyScreen.SelectedSub != null) - { - initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; - } + initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; } - else if (subList.SelectedData is SubmarineInfo subInfo) - { - initialMoney -= subInfo.Price; - } - initialMoney = Math.Max(initialMoney, isMultiplayer ? MultiPlayerCampaign.MinimumInitialMoney : 0); + initialMoney = Math.Max(initialMoney, MultiPlayerCampaign.MinimumInitialMoney); return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - if (!isMultiplayer) - { - CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) - { - OnClicked = (tb, userdata) => - { - CreateCustomizeWindow(); - return true; - } - }; - - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { GameMain.Instance.ShowCampaignDisclaimer(); return true; } - }; - disclaimerBtn.RectTransform.MaxSize = new Point((int)(30 * GUI.Scale)); - } - columnContainer.Recalculate(); leftColumn.Recalculate(); rightColumn.Recalculate(); @@ -306,55 +171,6 @@ namespace Barotrauma UpdateLoadMenu(saveFiles); } - private void CreateCustomizeWindow() - { - CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); - CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; - - CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) - { - RelativeSpacing = 0.1f - }; - - if (MapGenerationParams.Instance.RadiationParams != null) - { - bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) - { - Selected = prevRadiationToggleEnabled, - ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") - }; - } - var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - ToolTip = TextManager.Get("maxmissioncounttooltip") - }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true); - var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; - var maxMissionCountButtons = new GUIButton[2]; - maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") - { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } - }; - - string prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); - MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); - maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") - { - OnClicked = (button, obj) => - { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); - return true; - } - }; - maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); - } - private void CreateMultiplayerCampaignSubList(RectTransform parent) { GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) @@ -455,40 +271,6 @@ namespace Barotrauma } } - public void RandomizeSeed() - { - seedBox.Text = ToolBox.RandomSeed(8); - } - - private void FilterSubs(GUIListBox subList, string filter) - { - foreach (GUIComponent child in subList.Content.Children) - { - var sub = child.UserData as SubmarineInfo; - if (sub == null) { return; } - child.Visible = string.IsNullOrEmpty(filter) ? true : sub.DisplayName.ToLower().Contains(filter.ToLower()); - } - } - - private bool OnSubSelected(GUIComponent component, object obj) - { - if (subPreviewContainer == null) { return false; } - (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); - subPreviewContainer.ClearChildren(); - - if (!(obj is SubmarineInfo sub)) { return true; } -#if !DEBUG - if (!isMultiplayer && sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) - { - StartButton.Enabled = false; - return false; - } -#endif - StartButton.Enabled = true; - sub.CreatePreviewWindow(subPreviewContainer); - return true; - } - private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); @@ -516,24 +298,11 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void CreateDefaultSaveName() - { - string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer); - saveNameBox.Text = Path.GetFileNameWithoutExtension(savePath); - } - public void UpdateSubList(IEnumerable submarines) { List subsToShow; - if (!isMultiplayer && subFilter != CategoryFilter.All) - { - subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && s.IsVanillaSubmarine() == (subFilter == CategoryFilter.Vanilla)).ToList(); - } - else - { - string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); - } + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); subsToShow.Sort((s1, s2) => { @@ -596,10 +365,10 @@ namespace Barotrauma if (saveFiles == null) { - saveFiles = SaveUtil.GetSaveFiles(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer); + saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer); } - var leftColumn = new GUILayoutGroup(new RectTransform(isMultiplayer ? new Vector2(1.0f, 0.85f) : new Vector2(0.5f, 1.0f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.85f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) { Stretch = true, RelativeSpacing = 0.03f @@ -610,27 +379,6 @@ namespace Barotrauma OnSelected = SelectSaveFile }; - if (!isMultiplayer) - { - new GUIButton(new RectTransform(new Vector2(0.6f, 0.08f), leftColumn.RectTransform), TextManager.Get("showinfolder")) - { - OnClicked = (btn, userdata) => - { - try - { - ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); - } - catch (Exception e) - { - new GUIMessageBox( - TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); - } - return true; - } - }; - } - foreach (string saveFile in saveFiles) { string fileName = saveFile; @@ -649,39 +397,15 @@ namespace Barotrauma bool isCompatible = true; prevSaveFiles ??= new List(); - if (!isMultiplayer) - { - nameText.Text = Path.GetFileNameWithoutExtension(saveFile); - XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); - if (doc?.Root == null) - { - DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); - nameText.TextColor = GUI.Style.Red; - continue; - } - if (doc.Root.GetChildElement("multiplayercampaign") != null) - { - //multiplayer campaign save in the wrong folder -> don't show the save - saveList.Content.RemoveChild(saveFrame); - continue; - } - subName = doc.Root.GetAttributeString("submarine", ""); - saveTime = doc.Root.GetAttributeString("savetime", ""); - isCompatible = SaveUtil.IsSaveFileCompatible(doc); - contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); - prevSaveFiles?.Add(saveFile); - } - else - { - prevSaveFiles?.Add(saveFile); - string[] splitSaveFile = saveFile.Split(';'); - saveFrame.UserData = splitSaveFile[0]; - fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); - if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } - if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } - if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } - } + prevSaveFiles?.Add(saveFile); + string[] splitSaveFile = saveFile.Split(';'); + saveFrame.UserData = splitSaveFile[0]; + fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } + if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } + if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } + if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) { DateTime time = ToolBox.Epoch.ToDateTime(unixTime); @@ -748,10 +472,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } LoadGame?.Invoke(saveList.SelectedData as string); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); return true; }, Enabled = false @@ -768,72 +489,13 @@ namespace Barotrauma { string fileName = (string)obj; - if (isMultiplayer) + loadGameButton.Enabled = true; + deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; + deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; + if (deleteMpSaveButton.Visible) { - loadGameButton.Enabled = true; - deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; - deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; - if (deleteMpSaveButton.Visible) - { - deleteMpSaveButton.UserData = obj as string; - } - return true; + deleteMpSaveButton.UserData = obj as string; } - - XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); - if (doc?.Root == null) - { - DebugConsole.ThrowError("Error loading save file \"" + fileName + "\". The file may be corrupted."); - return false; - } - - loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(doc); - - RemoveSaveFrame(); - - string subName = doc.Root.GetAttributeString("submarine", ""); - string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); - if (long.TryParse(saveTime, out long unixTime)) - { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); - saveTime = time.ToString(); - } - - string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); - - var saveFileFrame = new GUIFrame(new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) - { - RelativeOffset = new Vector2(0.0f, 0.1f) - }, style: "InnerFrame") - { - UserData = "savefileframe" - }; - - var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) - { - RelativeOffset = new Vector2(0, 0.05f) - }, - Path.GetFileNameWithoutExtension(fileName), font: GUI.LargeFont, textAlignment: Alignment.Center); - titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); - - var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) - { - RelativeOffset = new Vector2(0, 0.1f) - }); - - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUI.SmallFont); - - new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) - { - RelativeOffset = new Vector2(0, 0.1f) - }, TextManager.Get("Delete"), style: "GUIButtonSmall") - { - UserData = fileName, - OnClicked = DeleteSave - }; - return true; } @@ -855,19 +517,5 @@ namespace Barotrauma return true; } - - private void RemoveSaveFrame() - { - GUIComponent prevFrame = null; - foreach (GUIComponent child in loadGameContainer.Children) - { - if (child.UserData as string != "savefileframe") continue; - - prevFrame = child; - break; - } - loadGameContainer.RemoveChild(prevFrame); - } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs new file mode 100644 index 000000000..17616623b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -0,0 +1,837 @@ +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using Barotrauma.IO; +using System.Linq; +using System.Xml.Linq; +using System.Globalization; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + class SinglePlayerCampaignSetupUI : CampaignSetupUI + { + public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } + + private GUIButton nextButton; + private GUILayoutGroup characterInfoColumns; + + public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + : base(newGameContainer, loadGameContainer) + { + UpdateNewGameMenu(submarines); + UpdateLoadMenu(saveFiles); + } + + private int currentPage = 0; + private GUIListBox pageContainer; + + public void Update() + { + float targetScroll = + (float)currentPage / ((float)pageContainer.Content.CountChildren - 1); + + pageContainer.BarScroll = MathHelper.Lerp(pageContainer.BarScroll, targetScroll, 0.2f); + if (MathUtils.NearlyEqual(pageContainer.BarScroll, targetScroll, 0.001f)) + { + pageContainer.BarScroll = targetScroll; + } + + for (int i=0; i + { + if (c is GUIDropDown dd) + { + dd.Dropped = false; + } + c.CanBeFocused = (i == currentPage); + }); + } + var previewListBox = subPreviewContainer.GetAllChildren().FirstOrDefault(); + previewListBox?.GetAllChildren()?.ForEach(c => + { + c.CanBeFocused = false; + }); + } + + private void UpdateNewGameMenu(IEnumerable submarines) + { + pageContainer = + new GUIListBox(new RectTransform(Vector2.One, newGameContainer.RectTransform), style: null, isHorizontal: true) + { + ScrollBarEnabled = false, + ScrollBarVisible = false, + HoverCursor = CursorState.Default + }; + + GUILayoutGroup createPageLayout() + { + var containerItem = + new GUIFrame(new RectTransform(Vector2.One, pageContainer.Content.RectTransform), style: null); + return new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, containerItem.RectTransform, + Anchor.Center)); + } + + CreateFirstPage(createPageLayout(), submarines); + CreateSecondPage(createPageLayout()); + + pageContainer.RecalculateChildren(); + pageContainer.GetAllChildren().ForEach(c => + { + c.ClampMouseRectToParent = true; + }); + pageContainer.GetAllChildren().ForEach(dd => + { + dd.ListBox.ClampMouseRectToParent = false; + dd.ListBox.Content.ClampMouseRectToParent = false; + }); + SetPage(0); + } + + private void CreateFirstPage(GUILayoutGroup firstPageLayout, IEnumerable submarines) + { + firstPageLayout.RelativeSpacing = 0.02f; + + var columnContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), firstPageLayout.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.015f + }; + + var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.5f, 1.0f), columnContainer.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.015f + }; + + columnContainer.Recalculate(); + + // New game left side + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + { + textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + + var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); + moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); + moddedDropdown.AddItem(TextManager.Get("servertag.modded.false"), CategoryFilter.Vanilla); + moddedDropdown.AddItem(TextManager.Get("customrank"), CategoryFilter.Custom); + moddedDropdown.Select(0); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; + + moddedDropdown.OnSelected = (component, data) => + { + searchBox.Text = string.Empty; + subFilter = (CategoryFilter)data; + UpdateSubList(SubmarineInfo.SavedSubmarines); + return true; + }; + + subList.OnSelected = OnSubSelected; + + // New game right side + subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)) + { + Stretch = true + }; + + var firstPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), + firstPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) + { + RelativeSpacing = 0.025f + }; + + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUI.Style.Font, textColor: GUI.Style.Green, textAlignment: Alignment.CenterLeft) + { + TextGetter = () => + { + int initialMoney = CampaignMode.InitialMoney; + if (subList.SelectedData is SubmarineInfo subInfo) + { + initialMoney -= subInfo.Price; + } + initialMoney = Math.Max(initialMoney, 0); + return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); + } + }; + + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) + { + OnClicked = (tb, userdata) => + { + CreateCustomizeWindow(); + return true; + } + }; + + nextButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), firstPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Next")) + { + OnClicked = (GUIButton btn, object userData) => + { + SetPage(1); + return false; + } + }; + + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") + { + IgnoreLayoutGroups = true, + OnClicked = (btn, userdata) => { GameMain.Instance.ShowCampaignDisclaimer(); return true; } + }; + disclaimerBtn.RectTransform.MaxSize = new Point((int)(30 * GUI.Scale)); + + columnContainer.Recalculate(); + leftColumn.Recalculate(); + rightColumn.Recalculate(); + + if (submarines != null) { UpdateSubList(submarines); } + } + + private void CreateSecondPage(GUILayoutGroup secondPageLayout) + { + secondPageLayout.RelativeSpacing = 0.01f; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), + TextManager.Get("Crew"), font: GUI.Style.SubHeadingFont, textAlignment: Alignment.TopLeft); + + characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), + secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) + { + RelativeSpacing = 0.2f + }; + + var backButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), secondPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Back")) + { + OnClicked = (GUIButton btn, object userData) => + { + SetPage(0); + return false; + } + }; + + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), secondPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) + { + OnClicked = FinishSetup + }; + } + + public void RandomizeCrew() + { + var characterInfos = new List<(CharacterInfo Info, JobPrefab Job)>(); + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + for (int i = 0; i < jobPrefab.InitialCount; i++) + { + var variant = Rand.Range(0, jobPrefab.Variants); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant), jobPrefab)); + } + } + + characterInfoColumns.ClearChildren(); + CharacterMenus?.ForEach(m => m.Dispose()); + CharacterMenus = new CharacterInfo.AppearanceCustomizationMenu[characterInfos.Count]; + + for (int i = 0; i < characterInfos.Count; i++) + { + var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), + characterInfoColumns.RectTransform)); + + var (characterInfo, job) = characterInfos[i]; + + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.275f), subLayout.RectTransform)); + + var jobTextContainer = + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), subLayout.RectTransform), style: null); + var jobText = new GUITextBlock(new RectTransform(Vector2.One, jobTextContainer.RectTransform), job.Name, job.UIColor); + + var characterName = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), subLayout.RectTransform)) + { + Text = characterInfo.Name, + UserData = "random" + }; + characterName.OnDeselected += (sender, key) => + { + if (string.IsNullOrWhiteSpace(sender.Text)) + { + characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced); + sender.Text = characterInfo.Name; + sender.UserData = "random"; + } + else + { + characterInfo.Name = sender.Text; + sender.UserData = "user"; + } + }; + characterName.OnEnterPressed += (sender, text) => + { + sender.Deselect(); + return false; + }; + + var customizationFrame = + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), subLayout.RectTransform), style: null); + CharacterMenus[i] = + new CharacterInfo.AppearanceCustomizationMenu(characterInfo, customizationFrame, hasIcon: false) + { + OnHeadSwitch = menu => + { + if (characterName.UserData is string ud && ud == "random") + { + characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced); + characterName.Text = characterInfo.Name; + characterName.UserData = "random"; + } + + StealRandomizeButton(menu, jobTextContainer); + } + }; + StealRandomizeButton(CharacterMenus[i], jobTextContainer); + } + } + + private void CreateCustomizeWindow() + { + CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); + CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; + + CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) + { + RelativeSpacing = 0.1f + }; + + if (MapGenerationParams.Instance.RadiationParams != null) + { + bool prevRadiationToggleEnabled = EnableRadiationToggle?.Selected ?? true; + EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + { + Selected = prevRadiationToggleEnabled, + ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") + }; + } + var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + ToolTip = TextManager.Get("maxmissioncounttooltip") + }; + var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true); + var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var maxMissionCountButtons = new GUIButton[2]; + maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + return true; + } + }; + + string prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); + MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), prevMaxMissionCountText, textAlignment: Alignment.Center, style: "GUITextBox"); + maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + return true; + } + }; + maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); + } + + private static void StealRandomizeButton(CharacterInfo.AppearanceCustomizationMenu menu, GUIComponent parent) + { + //This is just stupid + var randomizeButton = menu.RandomizeButton; + var oldButton = parent.GetChild(); + parent.RemoveChild(oldButton); + randomizeButton.RectTransform.Parent = parent.RectTransform; + randomizeButton.RectTransform.RelativeSize = Vector2.One * 1.3f; + } + + private bool FinishSetup(GUIButton btn, object userdata) + { + if (string.IsNullOrWhiteSpace(saveNameBox.Text)) + { + saveNameBox.Flash(GUI.Style.Red); + return false; + } + + SubmarineInfo selectedSub = null; + + if (!(subList.SelectedData is SubmarineInfo)) { return false; } + selectedSub = subList.SelectedData as SubmarineInfo; + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } + + if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + { + ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; + subList.SelectedComponent.CanBeFocused = false; + subList.Deselect(); + return false; + } + + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + + CampaignSettings settings = new CampaignSettings(); + settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; + if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) + { + settings.MaxMissionCount = missionCount; + } + else + { + settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) + { + if (!hasRequiredContentPackages) + { + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), + new string[] { 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); + } + return true; + }; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), + TextManager.Get("ShuttleWarning"), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = (button, obj) => + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + } + else + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + } + + return true; + } + + public void RandomizeSeed() + { + seedBox.Text = ToolBox.RandomSeed(8); + } + + private void FilterSubs(GUIListBox subList, string filter) + { + foreach (GUIComponent child in subList.Content.Children) + { + var sub = child.UserData as SubmarineInfo; + if (sub == null) { return; } + child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.ToLower().Contains(filter.ToLower()); + } + } + + private bool OnSubSelected(GUIComponent component, object obj) + { + if (subPreviewContainer == null) { return false; } + (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); + subPreviewContainer.ClearChildren(); + + if (!(obj is SubmarineInfo sub)) { return true; } +#if !DEBUG + if (sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) + { + SetPage(0); + nextButton.Enabled = false; + return false; + } +#endif + nextButton.Enabled = true; + sub.CreatePreviewWindow(subPreviewContainer); + return true; + } + + public void CreateDefaultSaveName() + { + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer); + saveNameBox.Text = Path.GetFileNameWithoutExtension(savePath); + } + + public void UpdateSubList(IEnumerable submarines) + { + List subsToShow; + if (subFilter != CategoryFilter.All) + { + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && s.IsVanillaSubmarine() == (subFilter == CategoryFilter.Vanilla)).ToList(); + } + else + { + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); + } + + subsToShow.Sort((s1, s2) => + { + int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; + int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; + return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); + }); + + subList.ClearChildren(); + + foreach (SubmarineInfo sub in subsToShow) + { + var textBlock = new GUITextBlock( + new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, + ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), style: "ListBoxElement") + { + ToolTip = sub.Description, + UserData = sub + }; + + if (!sub.RequiredContentPackagesInstalled) + { + textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); + textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; + } + + var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + { + TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, + ToolTip = textBlock.ToolTip + }; +#if !DEBUG + if (!GameMain.DebugDraw) + { + if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) + { + textBlock.CanBeFocused = false; + textBlock.TextColor *= 0.5f; + } + } +#endif + } + if (SubmarineInfo.SavedSubmarines.Any()) + { + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); + if (validSubs.Count > 0) + { + subList.Select(validSubs[Rand.Int(validSubs.Count)]); + } + } + } + + private List prevSaveFiles; + public void UpdateLoadMenu(IEnumerable saveFiles = null) + { + prevSaveFiles?.Clear(); + prevSaveFiles = null; + loadGameContainer.ClearChildren(); + + if (saveFiles == null) + { + saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); + } + + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f + }; + + saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) + { + OnSelected = SelectSaveFile + }; + + new GUIButton(new RectTransform(new Vector2(0.6f, 0.08f), leftColumn.RectTransform), TextManager.Get("showinfolder")) + { + OnClicked = (btn, userdata) => + { + try + { + ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); + } + catch (Exception e) + { + new GUIMessageBox( + TextManager.Get("error"), + TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); + } + return true; + } + }; + + foreach (string saveFile in saveFiles) + { + string fileName = saveFile; + string subName = ""; + string saveTime = ""; + string contentPackageStr = ""; + var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") + { + UserData = saveFile + }; + + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), "") + { + CanBeFocused = false + }; + + bool isCompatible = true; + prevSaveFiles ??= new List(); + + nameText.Text = Path.GetFileNameWithoutExtension(saveFile); + XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); + + if (doc?.Root == null) + { + DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); + nameText.TextColor = GUI.Style.Red; + continue; + } + if (doc.Root.GetChildElement("multiplayercampaign") != null) + { + //multiplayer campaign save in the wrong folder -> don't show the save + saveList.Content.RemoveChild(saveFrame); + continue; + } + subName = doc.Root.GetAttributeString("submarine", ""); + saveTime = doc.Root.GetAttributeString("savetime", ""); + isCompatible = SaveUtil.IsSaveFileCompatible(doc); + contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); + prevSaveFiles?.Add(saveFile); + if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) + { + DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + saveTime = time.ToString(); + } + if (!string.IsNullOrEmpty(contentPackageStr)) + { + List contentPackagePaths = contentPackageStr.Split('|').ToList(); + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + { + nameText.TextColor = GUI.Style.Red; + saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); + } + } + if (!isCompatible) + { + nameText.TextColor = GUI.Style.Red; + saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), + text: subName, font: GUI.SmallFont) + { + CanBeFocused = false, + UserData = fileName + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), + text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + { + CanBeFocused = false, + UserData = fileName + }; + } + + saveList.Content.RectTransform.SortChildren((c1, c2) => + { + string file1 = c1.GUIComponent.UserData as string; + string file2 = c2.GUIComponent.UserData as string; + DateTime file1WriteTime = DateTime.MinValue; + DateTime file2WriteTime = DateTime.MinValue; + try + { + file1WriteTime = File.GetLastWriteTime(file1); + } + catch + { + //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list + }; + try + { + file2WriteTime = File.GetLastWriteTime(file2); + } + catch + { + //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list + }; + return file2WriteTime.CompareTo(file1WriteTime); + }); + + loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) + { + OnClicked = (btn, obj) => + { + if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } + LoadGame?.Invoke(saveList.SelectedData as string); + return true; + }, + Enabled = false + }; + } + + private bool SelectSaveFile(GUIComponent component, object obj) + { + string fileName = (string)obj; + + XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); + if (doc?.Root == null) + { + DebugConsole.ThrowError("Error loading save file \"" + fileName + "\". The file may be corrupted."); + return false; + } + + loadGameButton.Enabled = SaveUtil.IsSaveFileCompatible(doc); + + RemoveSaveFrame(); + + string subName = doc.Root.GetAttributeString("submarine", ""); + string saveTime = doc.Root.GetAttributeString("savetime", "unknown"); + if (long.TryParse(saveTime, out long unixTime)) + { + DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + saveTime = time.ToString(); + } + + string mapseed = doc.Root.GetAttributeString("mapseed", "unknown"); + + var saveFileFrame = new GUIFrame(new RectTransform(new Vector2(0.45f, 0.6f), loadGameContainer.RectTransform, Anchor.TopRight) + { + RelativeOffset = new Vector2(0.0f, 0.1f) + }, style: "InnerFrame") + { + UserData = "savefileframe" + }; + + var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) + { + RelativeOffset = new Vector2(0, 0.05f) + }, + Path.GetFileNameWithoutExtension(fileName), font: GUI.LargeFont, textAlignment: Alignment.Center); + titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); + + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) + { + RelativeOffset = new Vector2(0, 0.1f) + }); + + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUI.SmallFont); + + new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) + { + RelativeOffset = new Vector2(0, 0.1f) + }, TextManager.Get("Delete"), style: "GUIButtonSmall") + { + UserData = fileName, + OnClicked = DeleteSave + }; + + return true; + } + + private bool DeleteSave(GUIButton button, object obj) + { + string saveFile = obj as string; + if (obj == null) { return false; } + + string header = TextManager.Get("deletedialoglabel"); + string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + + EventEditorScreen.AskForConfirmation(header, body, () => + { + SaveUtil.DeleteSave(saveFile); + prevSaveFiles?.RemoveAll(s => s.StartsWith(saveFile)); + UpdateLoadMenu(prevSaveFiles.ToList()); + return true; + }); + + return true; + } + + private void RemoveSaveFrame() + { + GUIComponent prevFrame = null; + foreach (GUIComponent child in loadGameContainer.Children) + { + if (child.UserData as string != "savefileframe") continue; + + prevFrame = child; + break; + } + loadGameContainer.RemoveChild(prevFrame); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 947077154..27efd044d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -628,7 +628,7 @@ namespace Barotrauma { TextGetter = () => { - return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.MaxMissionCount}"); + return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}"); } }; @@ -735,7 +735,7 @@ namespace Barotrauma private void UpdateMaxMissions(Location location) { - hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.MaxMissionCount; + hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.TotalMaxMissionCount; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 9f158fe81..0a553e0a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -182,7 +182,7 @@ namespace Barotrauma.CharacterEditor showSpritesheet = false; isFrozen = false; autoFreeze = false; - limbPairEditing = true; + limbPairEditing = false; uniformScaling = true; lockSpriteOrigin = true; lockSpritePosition = false; @@ -483,24 +483,20 @@ namespace Barotrauma.CharacterEditor // It's possible that the physics are disabled, because the angle widgets handle input logic in the draw method (which they shouldn't) character.AnimController.Collider.PhysEnabled = true; } - if (character.IsHumanoid) + animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; + if (animTestPoseToggle.Enabled) { - animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; - if (animTestPoseToggle.Enabled) + if (PlayerInput.KeyHit(Keys.X)) { - if (PlayerInput.KeyHit(Keys.X)) - { - SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); - } - } - else - { - animTestPoseToggle.Selected = false; + SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); } } + else + { + animTestPoseToggle.Selected = false; + } if (PlayerInput.KeyHit(InputType.Run)) { - // TODO: refactor this horrible hacky index manipulation mess int index = 0; bool isSwimming = character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast || character.AnimController.ForceSelectAnimationType == AnimationType.SwimSlow; bool isMovingFast = character.AnimController.ForceSelectAnimationType == AnimationType.Run || character.AnimController.ForceSelectAnimationType == AnimationType.SwimFast; @@ -508,23 +504,25 @@ namespace Barotrauma.CharacterEditor { if (isSwimming || !character.AnimController.CanWalk) { - index = !character.AnimController.CanWalk ? 0 : (int)AnimationType.SwimSlow - 1; + index = !character.AnimController.CanWalk ? (int)AnimationType.SwimFast : (int)AnimationType.SwimSlow; } else { - index = (int)AnimationType.Walk - 1; + index = (int)AnimationType.Walk; } + index -= 1; } else { if (isSwimming || !character.AnimController.CanWalk) { - index = !character.AnimController.CanWalk ? 1 : (int)AnimationType.SwimFast - 1; + index = !character.AnimController.CanWalk ? (int)AnimationType.SwimSlow : (int)AnimationType.SwimFast; } else { - index = (int)AnimationType.Run - 1; + index = (int)AnimationType.Run; } + index -= 1; } if (animSelection.SelectedIndex != index) { @@ -1072,7 +1070,7 @@ namespace Barotrauma.CharacterEditor { if (jointCreationMode == JointCreationMode.Create) { - jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden); if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero; @@ -1085,7 +1083,7 @@ namespace Barotrauma.CharacterEditor } else if (PlayerInput.PrimaryMouseButtonClicked()) { - jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; jointCreationMode = JointCreationMode.Create; } @@ -1094,7 +1092,7 @@ namespace Barotrauma.CharacterEditor { if (jointCreationMode == JointCreationMode.Create) { - jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden); if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 anchor1 = anchor1Pos ?? Vector2.Zero; @@ -1105,7 +1103,7 @@ namespace Barotrauma.CharacterEditor } else if (PlayerInput.PrimaryMouseButtonClicked()) { - jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); jointCreationMode = JointCreationMode.Create; } @@ -1185,8 +1183,15 @@ namespace Barotrauma.CharacterEditor private void CreateLimb(XElement newElement) { - var lastLimbElement = RagdollParams.MainElement.Elements("limb").Last(); - lastLimbElement.AddAfterSelf(newElement); + var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); + if (lastElement != null) + { + lastElement.AddAfterSelf(newElement); + } + else + { + RagdollParams.MainElement.AddFirst(newElement); + } var newLimbParams = new RagdollParams.LimbParams(newElement, RagdollParams); RagdollParams.Limbs.Add(newLimbParams); character.AnimController.Recreate(); @@ -1217,12 +1222,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("limb1anchor", $"{a1.X.Format(2)}, {a1.Y.Format(2)}"), new XAttribute("limb2anchor", $"{a2.X.Format(2)}, {a2.Y.Format(2)}") ); - var lastJointElement = RagdollParams.MainElement.Elements("joint").LastOrDefault(); - if (lastJointElement == null) - { - // If no joints exist, use the last limb element. - lastJointElement = RagdollParams.MainElement.Elements("limb").LastOrDefault(); - } + var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastJointElement == null) { DebugConsole.ThrowError(GetCharacterEditorTranslation("CantAddJointsNoLimbElements")); @@ -1581,7 +1581,6 @@ namespace Barotrauma.CharacterEditor Character.Controlled = character; SetWallCollisions(character.AnimController.forceStanding); CreateTextures(); - limbPairEditing = character.IsHumanoid; CreateGUI(); ClearWidgets(); ClearSelection(); @@ -1803,6 +1802,7 @@ namespace Barotrauma.CharacterEditor { case AnimationType.Walk: case AnimationType.Run: + case AnimationType.Crouch: if (!ragdollParams.CanWalk) { continue; } break; case AnimationType.SwimSlow: @@ -1819,8 +1819,8 @@ namespace Barotrauma.CharacterEditor { AllFiles.Add(configFilePath); } - limbPairEditing = false; SpawnCharacter(configFilePath, ragdollParams); + limbPairEditing = false; limbsToggle.Selected = true; recalculateColliderToggle.Selected = true; lockSpriteOriginToggle.Selected = false; @@ -2196,7 +2196,7 @@ namespace Barotrauma.CharacterEditor animTestPoseToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AnimationTestPose")) { Selected = character.AnimController.AnimationTestPose, - Enabled = character.IsHumanoid, + Enabled = true, OnSelected = box => { character.AnimController.AnimationTestPose = box.Selected; @@ -2599,11 +2599,15 @@ namespace Barotrauma.CharacterEditor var layoutGroupAnimation = new GUILayoutGroup(new RectTransform(Vector2.One, animationControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false }; var animationSelectionElement = new GUIFrame(new RectTransform(new Point(elementSize.X * 2 - (int)(5 * GUI.xScale), elementSize.Y), layoutGroupAnimation.RectTransform), style: null); var animationSelectionText = new GUITextBlock(new RectTransform(new Point(elementSize.X, elementSize.Y), animationSelectionElement.RectTransform), GetCharacterEditorTranslation("SelectedAnimation") + ": ", Color.WhiteSmoke, textAlignment: Alignment.Center); - animSelection = new GUIDropDown(new RectTransform(new Point((int)(100 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.TopRight), elementCount: 4); + animSelection = new GUIDropDown(new RectTransform(new Point((int)(100 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.TopRight), elementCount: 5); if (character.AnimController.CanWalk) { animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk); animSelection.AddItem(AnimationType.Run.ToString(), AnimationType.Run); + if (character.IsHumanoid) + { + animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); + } } animSelection.AddItem(AnimationType.SwimSlow.ToString(), AnimationType.SwimSlow); animSelection.AddItem(AnimationType.SwimFast.ToString(), AnimationType.SwimFast); @@ -2622,25 +2626,15 @@ namespace Barotrauma.CharacterEditor switch (character.AnimController.ForceSelectAnimationType) { case AnimationType.Walk: - character.AnimController.forceStanding = true; - character.ForceRun = false; - if (!wallCollisionsEnabled) - { - SetWallCollisions(true); - } - if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run) - { - TeleportTo(spawnPosition); - } - break; case AnimationType.Run: + case AnimationType.Crouch: character.AnimController.forceStanding = true; - character.ForceRun = true; + character.ForceRun = character.AnimController.ForceSelectAnimationType == AnimationType.Run; if (!wallCollisionsEnabled) { SetWallCollisions(true); } - if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run) + if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run && previousAnim != AnimationType.Crouch) { TeleportTo(spawnPosition); } @@ -2766,7 +2760,7 @@ namespace Barotrauma.CharacterEditor return false; } #endif - if (!string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) + if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; @@ -3046,21 +3040,24 @@ namespace Barotrauma.CharacterEditor loadBox.Buttons[1].OnClicked += (btn, data) => { string fileName = Path.GetFileNameWithoutExtension(selectedFile); - if (character.IsHumanoid) + if (character.IsHumanoid && character.AnimController is HumanoidAnimController humanAnimController) { switch (selectedType) { case AnimationType.Walk: - character.AnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); + humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); break; case AnimationType.Run: - character.AnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); + humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); + break; + case AnimationType.Crouch: + humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, fileName); break; case AnimationType.SwimSlow: - character.AnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); + humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); break; case AnimationType.SwimFast: - character.AnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); + humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); break; default: DebugConsole.ThrowError(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); @@ -3866,7 +3863,7 @@ namespace Barotrauma.CharacterEditor { // Head angle DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, - angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + 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 = GUI.Style.Red; if (animParams.IsGroundedAnimation) @@ -3975,7 +3972,7 @@ namespace Barotrauma.CharacterEditor } // Torso angle DrawRadialWidget(spriteBatch, SimToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, - angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + 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) { @@ -4078,7 +4075,7 @@ namespace Barotrauma.CharacterEditor if (tail != null && fishParams != null) { DrawRadialWidget(spriteBatch, SimToScreen(tail.SimPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, - angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + tail.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); } // Foot angle if (foot != null) @@ -4104,13 +4101,13 @@ namespace Barotrauma.CharacterEditor fishParams.FootAnglesInRadians[limb.Params.ID] = MathHelper.ToRadians(angle); TryUpdateAnimParam("footangles", fishParams.FootAngles); }, - circleRadius: 25, rotationOffset: collider.Rotation, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true); + circleRadius: 25, rotationOffset: -collider.Rotation + limb.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true); } } else if (humanParams != null) { DrawRadialWidget(spriteBatch, SimToScreen(foot.SimPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, - angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true); + angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + foot.Params.GetSpriteOrientation() * dir, clockWise: dir > 0, wrapAnglePi: true); } // Grounded only if (groundedParams != null) @@ -4152,7 +4149,7 @@ namespace Barotrauma.CharacterEditor float offset = 0.1f; w.refresh = () => { - var refPoint = SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanGroundedParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4167,7 +4164,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4731,7 +4728,7 @@ namespace Barotrauma.CharacterEditor rotation: 0, origin: orig, sourceRectangle: wearable.InheritSourceRect ? limb.ActiveSprite.SourceRect : wearable.Sprite.SourceRect, - scale: (wearable.InheritTextureScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom, + scale: (wearable.InheritScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom, effects: SpriteEffects.None, color: Color.White, layerDepth: 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 6d6d44024..5c41420ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -262,7 +262,7 @@ namespace Barotrauma.CharacterEditor { FileSelection.OnFileSelected = (file) => { - string relativePath = UpdaterUtil.GetRelativePath(Path.GetFullPath(file), Environment.CurrentDirectory); + string relativePath = Path.GetRelativePath(Environment.CurrentDirectory, Path.GetFullPath(file)); string destinationPath = relativePath; //copy file to XML path if it's not located relative to the game's files @@ -284,7 +284,7 @@ namespace Barotrauma.CharacterEditor } isTextureSelected = true; - texturePathElement.Text = destinationPath; + texturePathElement.Text = destinationPath.CleanUpPath(); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); @@ -431,7 +431,7 @@ namespace Barotrauma.CharacterEditor box.Header.Font = GUI.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = (int)(20 * GUI.Scale); - int elementSize = (int)(30 * GUI.Scale); + int elementSize = (int)(40 * GUI.Scale); var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(80 * GUI.xScale), box.Content.Rect.Height - (int)(200 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) { @@ -625,8 +625,12 @@ namespace Barotrauma.CharacterEditor var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), GetCharacterEditorTranslation("Joints"), font: GUI.SubHeadingFont); var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) - { RelativeOffset = new Vector2(0.15f, 0) }, style: null) - { CanBeFocused = false }; + { + RelativeOffset = new Vector2(0.15f, 0) + }, style: null) + { + CanBeFocused = false + }; var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { @@ -824,7 +828,7 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 2 }; + var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 16 }; var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name, font: GUI.SubHeadingFont); var idField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 75bbf5cfc..d85bf42ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -541,7 +541,7 @@ namespace Barotrauma private XElement? ExportXML() { - XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLower())); + XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLowerInvariant())); EditorNode? startNode = null; foreach (EditorNode eventNode in nodeList.Where(node => node is EventNode || node is SpecialNode)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 172e8f313..87491a291 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -25,6 +25,8 @@ namespace Barotrauma public Effect PostProcessEffect { get; private set; } public Effect GradientEffect { get; private set; } public Effect GrainEffect { get; private set; } + public Effect ThresholdTintEffect { get; private set; } + public Effect BlueprintEffect { get; set; } public GameScreen(GraphicsDevice graphics, ContentManager content) { @@ -37,19 +39,20 @@ namespace Barotrauma CreateRenderTargets(graphics); }; + Effect LoadEffect(string path) + => content.Load(path #if LINUX || OSX - //var blurEffect = content.Load("Effects/blurshader_opengl"); - damageEffect = content.Load("Effects/damageshader_opengl"); - PostProcessEffect = content.Load("Effects/postprocess_opengl"); - GradientEffect = content.Load("Effects/gradientshader_opengl"); - GrainEffect = content.Load("Effects/grainshader_opengl"); -#else - //var blurEffect = content.Load("Effects/blurshader"); - damageEffect = content.Load("Effects/damageshader"); - PostProcessEffect = content.Load("Effects/postprocess"); - GradientEffect = content.Load("Effects/gradientshader"); - GrainEffect = content.Load("Effects/grainshader"); + +"_opengl" #endif + ); + + //var blurEffect = LoadEffect("Effects/blurshader"); + damageEffect = LoadEffect("Effects/damageshader"); + PostProcessEffect = LoadEffect("Effects/postprocess"); + GradientEffect = LoadEffect("Effects/gradientshader"); + GrainEffect = LoadEffect("Effects/grainshader"); + ThresholdTintEffect = LoadEffect("Effects/thresholdtint"); + BlueprintEffect = LoadEffect("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageEffect.Parameters["xStencil"].SetValue(damageStencil); @@ -89,8 +92,7 @@ namespace Barotrauma } } - if (GameMain.GameSession != null) GameMain.GameSession.AddToGUIUpdateList(); - + GameMain.GameSession?.AddToGUIUpdateList(); Character.AddAllToGUIUpdateList(); base.AddToGUIUpdateList(); } @@ -137,7 +139,7 @@ namespace Barotrauma for (int i = 0; i < Submarine.MainSubs.Length; i++) { if (Submarine.MainSubs[i] == null) continue; - if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) continue; + if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) { continue; } Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; @@ -149,6 +151,14 @@ namespace Barotrauma } } + if (!GUI.DisableHUD) + { + foreach (Character c in Character.CharacterList) + { + c.DrawGUIMessages(spriteBatch, cam); + } + } + GUI.Draw(cam, spriteBatch); spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 62fcc7ba7..fe25d54b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -36,7 +36,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; private Sprite editingSprite; @@ -74,9 +74,7 @@ namespace Barotrauma ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); ruinParamsList.OnSelected += (GUIComponent component, object obj) => { - var ruinGenerationParams = obj as RuinGenerationParams; - editorContainer.ClearChildren(); - new SerializableEntityEditor(editorContainer.Content.RectTransform, ruinGenerationParams, false, true, elementHeight: 20); + CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); return true; }; @@ -95,101 +93,7 @@ namespace Barotrauma outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); outpostParamsList.OnSelected += (GUIComponent component, object obj) => { - var outpostGenerationParams = obj as OutpostGenerationParams; - editorContainer.ClearChildren(); - var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); - - // location type ------------------------- - - var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } - - var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) - { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); - if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) - { - locationTypeDropDown.SelectItem(locationType); - } - } - if (!outpostGenerationParams.AllowedLocationTypes.Any()) - { - locationTypeDropDown.SelectItem("any"); - } - - locationTypeDropDown.OnSelected += (_, __) => - { - outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); - locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); - return true; - }; - locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - - outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); - - // module count ------------------------- - - var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); - outpostParamsEditor.AddCustomContent(moduleLabel, 100); - - foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) - { - var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key), textAlignment: Alignment.CenterLeft); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) - { - MinValueInt = 0, - MaxValueInt = 100, - IntValue = moduleCount.Value, - OnValueChanged = (numInput) => - { - outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); - if (numInput.IntValue == 0) - { - outpostParamsList.Select(outpostParamsList.SelectedData); - } - } - }; - moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); - } - - // add module count ------------------------- - - var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); - - HashSet availableFlags = new HashSet(); - foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (var sub in SubmarineInfo.SavedSubmarines) - { - if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } - } - - var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), - text: TextManager.Get("leveleditor.addmoduletype")); - foreach (string flag in availableFlags) - { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - } - moduleTypeDropDown.OnSelected += (_, userdata) => - { - outpostGenerationParams.SetModuleCount(userdata as string, 1); - outpostParamsList.Select(outpostParamsList.SelectedData); - return true; - }; - addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); - + CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); return true; }; @@ -239,10 +143,12 @@ namespace Barotrauma editorContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)); - var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), isHorizontal: true); + var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), TextManager.Get("leveleditor.levelseed")); seedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), ToolBox.RandomSeed(8)); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -253,7 +159,7 @@ namespace Barotrauma GameMain.LightManager.ClearLights(); LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; - Level.Generate(levelData, mirror: false); + Level.Generate(levelData, mirror: mirrorLevel.Selected); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || cam.Position.X < 0 || cam.Position.Y < 0 || cam.Position.Y > Level.Loaded.Size.X || cam.Position.Y > Level.Loaded.Size.Y) { @@ -408,7 +314,7 @@ namespace Barotrauma editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); - foreach (RuinGenerationParams genParams in RuinGenerationParams.List) + foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -500,6 +406,104 @@ namespace Barotrauma } } + private void CreateOutpostGenerationParamsEditor(OutpostGenerationParams outpostGenerationParams) + { + editorContainer.ClearChildren(); + var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); + + // location type ------------------------- + + var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); + HashSet availableLocationTypes = new HashSet { "any" }; + foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } + + var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), + text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); + foreach (string locationType in availableLocationTypes) + { + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) + { + locationTypeDropDown.SelectItem(locationType); + } + } + if (!outpostGenerationParams.AllowedLocationTypes.Any()) + { + locationTypeDropDown.SelectItem("any"); + } + + locationTypeDropDown.OnSelected += (_, __) => + { + outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); + return true; + }; + locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); + + // module count ------------------------- + + var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); + outpostParamsEditor.AddCustomContent(moduleLabel, 100); + + foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) + { + var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key), textAlignment: Alignment.CenterLeft); + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + MaxValueInt = 100, + IntValue = moduleCount.Value, + OnValueChanged = (numInput) => + { + outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); + if (numInput.IntValue == 0) + { + outpostParamsList.Select(outpostParamsList.SelectedData); + } + } + }; + moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); + } + + // add module count ------------------------- + + var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); + + HashSet availableFlags = new HashSet(); + foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (var sub in SubmarineInfo.SavedSubmarines) + { + if (sub.OutpostModuleInfo == null) { continue; } + foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } + } + + var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), + text: TextManager.Get("leveleditor.addmoduletype")); + foreach (string flag in availableFlags) + { + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + } + moduleTypeDropDown.OnSelected += (_, userdata) => + { + outpostGenerationParams.SetModuleCount(userdata as string, 1); + outpostParamsList.Select(outpostParamsList.SelectedData); + return true; + }; + addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); + + } + private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) { editorContainer.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 12c1f9165..ffb0f46a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -40,7 +40,7 @@ namespace Barotrauma private readonly GUIFrame[] menuTabs; - private CampaignSetupUI campaignSetupUI; + private SinglePlayerCampaignSetupUI campaignSetupUI; private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; @@ -498,7 +498,7 @@ namespace Barotrauma menuTabs[(int)Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, style: null); - menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); CreateCampaignSetupUI(); @@ -657,6 +657,8 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.NewGame); }); return true; } + campaignSetupUI.RandomizeCrew(); + campaignSetupUI.SetPage(0); campaignSetupUI.CreateDefaultSaveName(); campaignSetupUI.RandomizeSeed(); campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); @@ -1048,24 +1050,32 @@ namespace Barotrauma if (selectedTab < Tab.Empty && menuTabs[(int)selectedTab] != null) { menuTabs[(int)selectedTab].AddToGUIUpdateList(); + switch (selectedTab) + { + case Tab.NewGame: + campaignSetupUI.CharacterMenus?.ForEach(m => m.AddToGUIUpdateList()); + break; + } } } public override void Update(double deltaTime) { -#if !DEBUG -#if USE_STEAM +#if !DEBUG && USE_STEAM if (GameMain.Config.UseSteamMatchmaking) { hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; -#endif -#else -#if USE_STEAM +#elif USE_STEAM steamWorkshopButton.Enabled = true; #endif -#endif + switch (selectedTab) + { + case Tab.NewGame: + campaignSetupUI.Update(); + break; + } } public void DrawBackground(GraphicsDevice graphics, SpriteBatch spriteBatch) @@ -1182,6 +1192,11 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, settings, mapSeed); + GameMain.GameSession.CrewManager.CharacterInfos.Clear(); + foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) + { + GameMain.GameSession.CrewManager.AddCharacterInfo(characterInfo); + } ((SinglePlayerCampaign)GameMain.GameSession.GameMode).LoadNewLevel(); } @@ -1209,7 +1224,7 @@ namespace Barotrauma menuTabs[(int)Tab.NewGame].ClearChildren(); menuTabs[(int)Tab.LoadGame].ClearChildren(); - var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.025f) }) + var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f @@ -1217,30 +1232,14 @@ namespace Barotrauma var newGameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.95f), innerNewGame.RectTransform, Anchor.Center), style: "InnerFrame"); - var paddedNewGame = new GUIFrame(new RectTransform(new Vector2(0.95f), newGameContent.RectTransform, Anchor.Center), style: null); var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, SubmarineInfo.SavedSubmarines) + campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) { LoadGame = LoadGame, StartNewGame = StartGame }; - - var startButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.BottomRight) - { - RelativeSpacing = 0.05f - }; - campaignSetupUI.StartButton.RectTransform.Parent = startButtonContainer.RectTransform; - campaignSetupUI.StartButton.RectTransform.MinSize = new Point( - (int)(campaignSetupUI.StartButton.TextBlock.TextSize.X * 1.5f), - campaignSetupUI.StartButton.RectTransform.MinSize.Y); - startButtonContainer.RectTransform.MinSize = new Point(0, campaignSetupUI.StartButton.RectTransform.MinSize.Y); - if (campaignSetupUI.CampaignCustomizeButton != null) - { - campaignSetupUI.CampaignCustomizeButton.RectTransform.Parent = startButtonContainer.RectTransform; - } - campaignSetupUI.InitialMoneyText.RectTransform.Parent = startButtonContainer.RectTransform; } private void CreateHostServerFields() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 7e9f8c97a..40b99af75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -12,7 +12,6 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private readonly List characterSprites = new List(); //private readonly List jobPreferenceSprites = new List(); private readonly GUIFrame infoFrame, modeFrame; @@ -65,7 +64,7 @@ namespace Barotrauma private readonly GUITickBox[] missionTypeTickBoxes; private readonly GUIListBox missionTypeList; - + public GUITextBox SeedBox { get; private set; @@ -78,11 +77,12 @@ namespace Barotrauma public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; - + private readonly GUIFrame playerInfoContainer; private GUILayoutGroup infoContainer; private GUIComponent changesPendingText; + private bool createPendingChangesText = true; public GUIButton PlayerFrame; private readonly GUIComponent subPreviewContainer; @@ -103,11 +103,12 @@ namespace Barotrauma private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; - public GUIListBox HeadSelectionList; + public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu; public GUIFrame JobSelectionFrame; + public GUIFrame JobPreferenceContainer; public GUIListBox JobList; - + private float autoRestartTimer; //persistent characterinfo provided by the server @@ -142,7 +143,7 @@ namespace Barotrauma get; private set; } - + public GUITextBox ServerMessage { get; @@ -238,7 +239,7 @@ namespace Barotrauma get { return shuttleList.SelectedData as SubmarineInfo; } } - public CampaignSetupUI CampaignSetupUI; + public MultiPlayerCampaignSetupUI CampaignSetupUI; public List CampaignSubmarines = new List(); // Passed onto the gamesession when created @@ -340,7 +341,7 @@ namespace Barotrauma Stretch = true, RelativeSpacing = panelSpacing }; - + GUILayoutGroup panelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), panelContainer.RectTransform)) { Stretch = true, @@ -413,7 +414,7 @@ namespace Barotrauma 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), "", + FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") { @@ -541,7 +542,7 @@ namespace Barotrauma // Chat input - var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), + var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true @@ -604,7 +605,7 @@ namespace Barotrauma Font = GUI.SmallFont }; - roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), + roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true @@ -650,7 +651,7 @@ namespace Barotrauma return true; } }; - clientDisabledElements.Add(autoRestartBoxContainer); + clientDisabledElements.Add(autoRestartBoxContainer); //-------------------------------------------------------------------------------------------------------------------------------- //infoframe contents @@ -659,7 +660,7 @@ namespace Barotrauma //server info ------------------------------------------------------------------ // Server Info Header - GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), + GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, @@ -813,7 +814,7 @@ namespace Barotrauma shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) { - Selected = true, + Selected = true, OnSelected = (GUITickBox box) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); @@ -851,13 +852,13 @@ namespace Barotrauma //------------------------------------------------------------------------------------------------------------------ // 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 @@ -874,7 +875,7 @@ namespace Barotrauma { OnSelected = VotableClicked }; - + foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) { continue; } @@ -900,7 +901,7 @@ namespace Barotrauma 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) }, + 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)); @@ -928,17 +929,17 @@ namespace Barotrauma OnClicked = (_, __) => { CoroutineManager.StartCoroutine(WaitForStartRound(ContinueCampaignButton), "WaitForStartRound"); - GameMain.Client?.RequestStartRound(true); - return true; + 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 = (_, __) => + OnClicked = (_, __) => { - GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); - return true; + GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + return true; } }; @@ -950,7 +951,7 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("MissionType"), font: GUI.SubHeadingFont); missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { @@ -981,28 +982,30 @@ namespace Barotrauma Visible = false, CanBeFocused = false }; - continue; } - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), - TextManager.Get("MissionType." + missionType.ToString())) + else { - UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), - OnSelected = (tickbox) => + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + TextManager.Get("MissionType." + missionType.ToString())) { - 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; + UserData = (int)missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), + 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 + // settings panel //------------------------------------------------------------------ GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) @@ -1083,7 +1086,7 @@ namespace Barotrauma } }; - traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), + traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), textAlignment: Alignment.Center, style: "GUITextBox"); traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleRight") { @@ -1249,12 +1252,10 @@ namespace Barotrauma { chatInput.Deselect(); CampaignCharacterDiscarded = false; - HeadSelectionList = null; + + CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - foreach (Sprite sprite in characterSprites) { sprite.Remove(); } - characterSprites.Clear(); - /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ } @@ -1263,8 +1264,8 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) { return; } - if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } - if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } + CharacterAppearanceCustomizationMenu?.Dispose(); + JobSelectionFrame = null; infoFrameContent.Recalculate(); @@ -1302,7 +1303,7 @@ namespace Barotrauma { spectateButton.Visible = false; } - SetSpectate(spectateBox.Selected); + SetSpectate(spectateBox.Selected); if (GameMain.Client != null) { @@ -1324,7 +1325,7 @@ namespace Barotrauma { publicOrPrivate.Text = isPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); } - + public void RefreshEnabledElements() { ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1347,7 +1348,7 @@ namespace Barotrauma button.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); } - traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = + traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1360,12 +1361,13 @@ namespace Barotrauma ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); + SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); shuttleList.Enabled = shuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); + roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; @@ -1382,7 +1384,7 @@ namespace Barotrauma } public void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo) - { + { if (newCampaignCharacterInfo != null) { if (CampaignCharacterDiscarded) { return; } @@ -1404,40 +1406,35 @@ namespace Barotrauma UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContainer); } - public void CreatePlayerFrame(GUIComponent parent) + public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, - allowEditing: campaignCharacterInfo == null, - parent: parent); + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, + allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, + parent: parent, + createPendingText: createPendingText); } - private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent) + private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) { + createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); - characterInfo.RecreateHead( - GameMain.Config.CharacterHeadIndex, - GameMain.Config.CharacterRace, - GameMain.Config.CharacterGender, - GameMain.Config.CharacterHairIndex, - GameMain.Config.CharacterBeardIndex, - GameMain.Config.CharacterMoustacheIndex, - GameMain.Config.CharacterFaceAttachmentIndex); + characterInfo.RecreateHead(GameMain.Config.PlayerCharacterCustomization); GameMain.Client.CharacterInfo = characterInfo; - characterInfo.OmitJobInPortraitClothing = true; + characterInfo.OmitJobInPortraitClothing = false; } parent.ClearChildren(); bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; - infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) + infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.97f : 0.92f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.025f, + RelativeSpacing = 0.0f, Stretch = true, - UserData = characterInfo + UserData = characterInfo }; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; @@ -1453,6 +1450,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) { MaxTextLength = Client.MaxNameLength, @@ -1476,7 +1474,10 @@ namespace Barotrauma { GameMain.Client.PendingName = tb.Text; TabMenu.PendingChanges = true; - CreateChangesPendingText(); + if (createPendingText) + { + CreateChangesPendingText(); + } } else { @@ -1484,27 +1485,27 @@ namespace Barotrauma } GameMain.Client.SetName(tb.Text); - }; + } }; - - new GUICustomComponent(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter), - onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), infoContainer.RectTransform), style: null); + if (allowEditing) { - GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.016f), infoContainer.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; - jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("JobPreferences"), style: "GUITabButton") { Selected = true, OnClicked = SelectJobPreferencesTab }; - appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("CharacterAppearance"), style: "GUITabButton") { OnClicked = SelectAppearanceTab @@ -1517,7 +1518,10 @@ namespace Barotrauma characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), infoContainer.RectTransform), style: null); characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription; - JobList = new GUIListBox(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), true) + JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), + style: "GUIFrameListBox"); + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0f, 0.025f) }); + JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, OnSelected = (child, obj) => @@ -1553,7 +1557,7 @@ namespace Barotrauma }; } - UpdateJobPreferences(JobList); + UpdateJobPreferences(); appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") { @@ -1563,6 +1567,8 @@ namespace Barotrauma } else { + characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, @@ -1587,17 +1593,10 @@ namespace Barotrauma IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => { - var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - confirmation.Buttons[0].OnClicked += confirmation.Close; - confirmation.Buttons[0].OnClicked += (btn2, userdata2) => + TryDiscardCampaignCharacter(() => { - CampaignCharacterDiscarded = true; - campaignCharacterInfo = null; UpdatePlayerFrame(null, true, parent); - return true; - }; - confirmation.Buttons[1].OnClicked += confirmation.Close; + }); return true; } }; @@ -1675,10 +1674,25 @@ namespace Barotrauma }; } } - + + public void TryDiscardCampaignCharacter(Action onYes) + { + var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), + new[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmation.Buttons[0].OnClicked += confirmation.Close; + confirmation.Buttons[0].OnClicked += (btn2, userdata2) => + { + CampaignCharacterDiscarded = true; + campaignCharacterInfo = null; + onYes(); + return true; + }; + confirmation.Buttons[1].OnClicked += confirmation.Close; + } + private void CreateChangesPendingText() { - if (changesPendingText != null || infoContainer == null) { return; } + if (!createPendingChangesText || changesPendingText != null || infoContainer == 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) }, style: "OuterGlow") @@ -1686,14 +1700,29 @@ namespace Barotrauma Color = Color.Black, IgnoreLayoutGroups = true }; - var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), + var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null); changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f)); } + public static void CreateChangesPendingFrame(GUIComponent parent) + { + parent.ClearChildren(); + GUIFrame changesPendingFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), + style: "OuterGlow") + { + Color = Color.Black + }; + new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center), + TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) + { + AutoScaleHorizontal = true + }; + } + private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot) { - jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(500 * GUI.Scale), (int)(200 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), + jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") { UserData = new Pair(jobPrefab, variant) @@ -1706,26 +1735,17 @@ namespace Barotrauma AbsoluteSpacing = (int)(15 * GUI.Scale) }; - string name = - TextManager.Get("jobname." + jobPrefab.Identifier + (variant + 1), returnNull: true, fallBackTag: "jobname." + jobPrefab.Identifier) ?? - ""; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), name, font: GUI.SubHeadingFont); - - string description = - TextManager.Get("jobdescription." + jobPrefab.Identifier + (variant + 1), returnNull: true, fallBackTag: "jobdescription." + jobPrefab.Identifier) ?? - ""; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), description, wrap: true, font: GUI.SmallFont); - - var itemIdentifiers = jobPrefab.ItemIdentifiers[variant] - .Distinct() - .Where(id => jobPrefab.ShowItemPreview[variant][id]); + var itemIdentifiers = jobPrefab.PreviewItems[variant] + .Where(it => it.ShowPreview) + .Select(it => it.ItemIdentifier) + .Distinct(); int itemsPerRow = 5; int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); - new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomLeft), + new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter), onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new Pair(jobPrefab, variant), itemsPerRow); }); jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); @@ -1767,7 +1787,7 @@ namespace Barotrauma if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; } // Hide spectate tickbox if spectating is not allowed - spectateBox.Visible = allowSpectating; + spectateBox.Visible = allowSpectating; } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1786,7 +1806,7 @@ namespace Barotrauma if (subList == null) { return; } subList.ClearChildren(); - + foreach (SubmarineInfo sub in submarines) { AddSubmarine(subList, sub); @@ -1872,15 +1892,15 @@ namespace Barotrauma UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, ToolTip = subTextBlock.RawToolTip - }; + }; } - + } public bool VotableClicked(GUIComponent component, object userData) { if (GameMain.Client == null) { return false; } - + VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { @@ -1972,7 +1992,7 @@ namespace Barotrauma return true; } - + public void AddPlayer(Client client) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), playerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, @@ -1996,8 +2016,8 @@ namespace Barotrauma OverrideState = GUIComponent.ComponentState.None, HoverColor = Color.White }; - - new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, + + new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIconDisabled") { UserData = "soundicondisabled", @@ -2020,7 +2040,8 @@ namespace Barotrauma var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); if (playerFrame == null) { return; } playerFrame.Text = client.Name; - + + playerFrame.ToolTip = ""; Color color = Color.White; if (SelectedMode == GameModePreset.PvP) { @@ -2028,15 +2049,28 @@ namespace Barotrauma { case CharacterTeamType.Team1: color = new Color(0, 110, 150, 255); + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team1")); break; case CharacterTeamType.Team2: color = new Color(150, 110, 0, 255); + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team2")); + break; + default: + playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("none")); break; } } - else if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) + else { - color = JobPrefab.Prefabs[client.PreferredJob].UIColor; + if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) + { + color = JobPrefab.Prefabs[client.PreferredJob].UIColor; + playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", JobPrefab.Prefabs[client.PreferredJob].Name); + } + else + { + playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", TextManager.Get("none")); + } } playerFrame.Color = color * 0.4f; playerFrame.HoverColor = color * 0.6f; @@ -2114,16 +2148,16 @@ namespace Barotrauma { Stretch = true }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), + + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), text: selectedClient.Name, font: GUI.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); if (hasManagePermissions) { PlayerFrame.UserData = selectedClient; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank"), font: GUI.SubHeadingFont); var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank")) @@ -2163,7 +2197,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUI.SubHeadingFont); - var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), + var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUI.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel); @@ -2308,7 +2342,7 @@ namespace Barotrauma var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - + if (!myClient) { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) @@ -2332,7 +2366,7 @@ namespace Barotrauma if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) { - if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && + if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), @@ -2399,7 +2433,7 @@ namespace Barotrauma } buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y)); - + if (buttonAreaTop != null) { if (buttonAreaTop.CountChildren == 0) @@ -2432,7 +2466,7 @@ namespace Barotrauma public void KickPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } - GameMain.Client.CreateKickReasonPrompt(client.Name, false); + GameMain.Client.CreateKickReasonPrompt(client.Name, false); } public void BanPlayer(Client client) @@ -2446,15 +2480,15 @@ namespace Barotrauma if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true, rangeBan: true); } - + public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); - + //CampaignSetupUI?.AddToGUIUpdateList(); JobInfoFrame?.AddToGUIUpdateList(); - HeadSelectionList?.AddToGUIUpdateList(); + CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); JobSelectionFrame?.AddToGUIUpdateList(); } @@ -2524,14 +2558,11 @@ namespace Barotrauma } } - if (HeadSelectionList != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(HeadSelectionList)) - { - HeadSelectionList.Visible = false; - } + CharacterAppearanceCustomizationMenu?.Update(); if (JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(JobSelectionFrame)) { JobList.Deselect(); - JobSelectionFrame.Visible = false; + JobSelectionFrame.Visible = false; } if (GUI.MouseOn?.UserData is Pair jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") @@ -2557,7 +2588,7 @@ namespace Barotrauma GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - + GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } @@ -2568,7 +2599,7 @@ namespace Barotrauma if (GameMain.NetworkMember?.ServerSettings == null) { return; } PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; - if ((int)playStyle < 0 || + if ((int)playStyle < 0 || (int)playStyle >= ServerListScreen.PlayStyleBanners.Length) { return; @@ -2594,9 +2625,10 @@ namespace Barotrauma private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, Pair jobPrefab, int itemsPerRow) { - var itemIdentifiers = jobPrefab.First.ItemIdentifiers[jobPrefab.Second] - .Distinct() - .Where(id => jobPrefab.First.ShowItemPreview[jobPrefab.Second][id]); + var itemIdentifiers = jobPrefab.First.PreviewItems[jobPrefab.Second] + .Where(it => it.ShowPreview) + .Select(it => it.ItemIdentifier) + .Distinct(); Point slotSize = new Point(component.Rect.Height); int spacing = (int)(5 * GUI.Scale); @@ -2615,7 +2647,7 @@ namespace Barotrauma int i = 0; Rectangle tooltipRect = Rectangle.Empty; string tooltip = null; - foreach (string itemIdentifier in itemIdentifiers) + foreach (var itemIdentifier in itemIdentifiers) { if (!(MapEntityPrefab.Find(null, identifier: itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)) { continue; } @@ -2634,7 +2666,7 @@ namespace Barotrauma float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f; icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale); - int count = jobPrefab.First.ItemIdentifiers[jobPrefab.Second].Count(id => id == itemIdentifier); + int count = jobPrefab.First.PreviewItems[jobPrefab.Second].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); if (count > 1) { string itemCountText = "x" + count; @@ -2693,9 +2725,9 @@ namespace Barotrauma msg.RectTransform.SizeChanged += Recalculate; } - if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) - { - chatBox.BarScroll = 1.0f; + if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) + { + chatBox.BarScroll = 1.0f; } } @@ -2704,212 +2736,56 @@ namespace Barotrauma jobPreferencesButton.Selected = true; appearanceButton.Selected = false; - JobList.Visible = true; + JobPreferenceContainer.Visible = true; appearanceFrame.Visible = false; return false; } - private bool SelectAppearanceTab(GUIButton button, object userData) + private bool SelectAppearanceTab(GUIButton button, object _) { jobPreferencesButton.Selected = false; appearanceButton.Selected = true; - JobList.Visible = false; + JobPreferenceContainer.Visible = false; appearanceFrame.Visible = true; appearanceFrame.ClearChildren(); - if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } - - GUIButton maleButton = null; - GUIButton femaleButton = null; var info = GameMain.Client.CharacterInfo; - - GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), appearanceFrame.RectTransform, Anchor.Center)) + CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { - RelativeSpacing = 0.05f - }; - - Vector2 elementSize = new Vector2(1.0f, 0.18f); - - GUILayoutGroup genderContainer = new GUILayoutGroup(new RectTransform(elementSize, content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), TextManager.Get("Gender"), font: GUI.SubHeadingFont); - maleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Male"), style: "ListBoxElement") - { - UserData = Gender.Male, - OnClicked = OpenHeadSelection, - Selected = info.Gender == Gender.Male - }; - femaleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Female"), style: "ListBoxElement") - { - UserData = Gender.Female, - OnClicked = OpenHeadSelection, - Selected = info.Gender == Gender.Female - }; - - int hairCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Hair, info.HeadSpriteId).Count(); - if (hairCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Hair"), font: GUI.SubHeadingFont); - var hairSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") + OnHeadSwitch = menu => { - Range = new Vector2(0, hairCount), - StepValue = 1, - BarScrollValue = info.HairIndex, - OnMoved = SwitchHair, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(hairCount + 1) - }; - } - - int beardCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Beard, info.HeadSpriteId).Count(); - if (beardCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Beard"), font: GUI.SubHeadingFont); - var beardSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") + StoreHead(true); + UpdateJobPreferences(); + SelectAppearanceTab(button, _); + }, + OnSliderMoved = (bar, scroll) => { - Range = new Vector2(0, beardCount), - StepValue = 1, - BarScrollValue = info.BeardIndex, - OnMoved = SwitchBeard, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(beardCount + 1) - }; - } - - int moustacheCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Moustache, info.HeadSpriteId).Count(); - if (moustacheCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Moustache"), font: GUI.SubHeadingFont); - var moustacheSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") - { - Range = new Vector2(0, moustacheCount), - StepValue = 1, - BarScrollValue = info.MoustacheIndex, - OnMoved = SwitchMoustache, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(moustacheCount + 1) - }; - } - - int faceAttachmentCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.FaceAttachment, info.HeadSpriteId).Count(); - if (faceAttachmentCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Accessories"), font: GUI.SubHeadingFont); - var faceAttachmentSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") - { - Range = new Vector2(0, faceAttachmentCount), - StepValue = 1, - BarScrollValue = info.FaceAttachmentIndex, - OnMoved = SwitchFaceAttachment, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(faceAttachmentCount + 1) - }; - } + StoreHead(false); + return false; + }, + OnSliderReleased = SaveHead + }; return false; } - - private bool OpenHeadSelection(GUIButton button, object userData) + + private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); + private bool StoreHead(bool save) { - Gender selectedGender = (Gender)userData; - if (HeadSelectionList != null) + GameMain.Config.PlayerCharacterCustomization = GameMain.Client.CharacterInfo.Head; + if (save) { - HeadSelectionList.Visible = true; - foreach (GUIComponent child in HeadSelectionList.Content.Children) + if (GameMain.GameSession?.IsRunning ?? false) { - child.Visible = (Gender)child.UserData == selectedGender; - child.Children.ForEach(c => c.Visible = ((Tuple)c.UserData).Item1 == selectedGender); + TabMenu.PendingChanges = true; + CreateChangesPendingText(); } - return true; + GameMain.Config.SaveNewPlayerConfig(); } - - var info = GameMain.Client.CharacterInfo; - - HeadSelectionList = new GUIListBox( - new RectTransform(new Point(characterInfoFrame.Rect.Width, (characterInfoFrame.Rect.Bottom - button.Rect.Bottom) + characterInfoFrame.Rect.Height * 2), GUI.Canvas) - { - AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - characterInfoFrame.Rect.Width, button.Rect.Bottom) - }); - - characterInfoFrame.RectTransform.SizeChanged += () => - { - if (characterInfoFrame == null || HeadSelectionList?.RectTransform == null || button == null) { return; } - HeadSelectionList.RectTransform.Resize(new Point(characterInfoFrame.Rect.Width, (characterInfoFrame.Rect.Bottom - button.Rect.Bottom) + characterInfoFrame.Rect.Height * 2)); - HeadSelectionList.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - characterInfoFrame.Rect.Width, button.Rect.Bottom); - if (SelectedSub != null) { CreateSubPreview(SelectedSub); } - }; - - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) - { - UserData = "outerglow", - CanBeFocused = false - }; - - GUILayoutGroup row = null; - int itemsInRow = 0; - - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); - XElement headSpriteElement = headElement.Element("sprite"); - string spritePathWithTags = headSpriteElement.Attribute("texture").Value; - - var characterConfigElement = info.CharacterConfigElement; - - var heads = info.Heads; - if (heads != null) - { - row = null; - itemsInRow = 0; - foreach (var head in heads) - { - var headPreset = head.Key; - Gender gender = headPreset.Gender; - Race race = headPreset.Race; - int headIndex = headPreset.ID; - - string spritePath = spritePathWithTags - .Replace("[GENDER]", gender.ToString().ToLowerInvariant()) - .Replace("[RACE]", race.ToString().ToLowerInvariant()); - - if (!File.Exists(spritePath)) { continue; } - - Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); - headSprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.Value.ToPoint()), headSprite.SourceRect.Size); - characterSprites.Add(headSprite); - - if (itemsInRow >= 4 || row == null || gender != (Gender)row.UserData) - { - row = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) - { - UserData = gender, - Visible = gender == selectedGender - }; - itemsInRow = 0; - } - - var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), style: "ListBoxElementSquare") - { - OutlineColor = Color.White * 0.5f, - PressedColor = Color.White * 0.5f, - UserData = new Tuple(gender, race, headIndex), - OnClicked = SwitchHead, - Selected = gender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, - Visible = gender == selectedGender - }; - - new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); - itemsInRow++; - } - } - - return false; + return true; } private bool SwitchJob(GUIButton _, object obj) @@ -2942,7 +2818,7 @@ namespace Barotrauma } } - UpdateJobPreferences(JobList); + UpdateJobPreferences(); if (moveToNext) { @@ -2973,14 +2849,14 @@ namespace Barotrauma return true; } - Point frameSize = new Point(characterInfoFrame.Rect.Width, characterInfoFrame.Rect.Height * 2); + Point frameSize = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f)); JobSelectionFrame = new GUIFrame(new RectTransform(frameSize, GUI.Canvas, Anchor.TopLeft) { AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - frameSize.X, characterInfoFrame.Rect.Bottom) }, style:"GUIFrameListBox"); characterInfoFrame.RectTransform.SizeChanged += () => { if (characterInfoFrame == null || JobSelectionFrame?.RectTransform == null) { return; } - Point size = new Point(characterInfoFrame.Rect.Width, characterInfoFrame.Rect.Height * 2); + Point size = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f)); JobSelectionFrame.RectTransform.Resize(size); JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - size.X, characterInfoFrame.Rect.Bottom); }; @@ -3097,7 +2973,7 @@ namespace Barotrauma { Pair sprite = outfitPreview.Sprites[j]; float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X; - retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) + retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) { RelativeOffset = sprite.Second / outfitPreview.Dimensions }, sprite.First, scaleToFit: true) { PressedColor = Color.White, @@ -3136,88 +3012,6 @@ namespace Barotrauma return retVal; } - private bool SwitchHead(GUIButton button, object obj) - { - var info = GameMain.Client.CharacterInfo; - - Gender gender = ((Tuple)obj).Item1; - Race race = ((Tuple)obj).Item2; - int id = ((Tuple)obj).Item3; - - if (gender != info.Gender || race != info.Race || id != info.HeadSpriteId) - { - info.Head = new CharacterInfo.HeadInfo(id, gender, race); - info.ReloadHeadAttachments(); - } - StoreHead(true); - - UpdateJobPreferences(JobList); - - SelectAppearanceTab(button, obj); - - return true; - } - - private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); - private bool SwitchHair(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Hair); - private bool SwitchBeard(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Beard); - private bool SwitchMoustache(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Moustache); - private bool SwitchFaceAttachment(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.FaceAttachment); - private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) - { - var info = GameMain.Client.CharacterInfo; - int index = (int)scrollBar.BarScrollValue; - switch (type) - { - case WearableType.Beard: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, index, info.MoustacheIndex, info.FaceAttachmentIndex); - break; - case WearableType.FaceAttachment: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, info.MoustacheIndex, index); - break; - case WearableType.Hair: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, index, info.BeardIndex, info.MoustacheIndex, info.FaceAttachmentIndex); - break; - case WearableType.Moustache: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, index, info.FaceAttachmentIndex); - break; - default: - DebugConsole.ThrowError($"Wearable type not implemented: {type}"); - return false; - } - info.ReloadHeadAttachments(); - StoreHead(false); - return true; - } - - private bool StoreHead(bool save) - { - var info = GameMain.Client.CharacterInfo; - var config = GameMain.Config; - - config.CharacterRace = info.Race; - config.CharacterGender = info.Gender; - config.CharacterHeadIndex = info.HeadSpriteId; - config.CharacterHairIndex = info.HairIndex; - config.CharacterBeardIndex = info.BeardIndex; - config.CharacterMoustacheIndex = info.MoustacheIndex; - config.CharacterFaceAttachmentIndex = info.FaceAttachmentIndex; - - if (save) - { - if (GameMain.GameSession?.IsRunning ?? false) - { - TabMenu.PendingChanges = true; - CreateChangesPendingText(); - } - - GameMain.Config.SaveNewPlayerConfig(); - } - - return true; - } - - public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } @@ -3322,10 +3116,10 @@ namespace Barotrauma ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; - StartButton.Visible = - GameMain.Client.HasPermission(ClientPermissions.ManageRound) && - !GameMain.Client.GameStarted && - !CampaignSetupFrame.Visible && + StartButton.Visible = + GameMain.Client.HasPermission(ClientPermissions.ManageRound) && + !GameMain.Client.GameStarted && + !CampaignSetupFrame.Visible && !CampaignFrame.Visible; } @@ -3390,7 +3184,7 @@ namespace Barotrauma OnClicked = CloseJobInfo }; JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; - + return true; } @@ -3400,8 +3194,13 @@ namespace Barotrauma return true; } - private void UpdateJobPreferences(GUIListBox listBox) + private void UpdateJobPreferences() { + GUICustomComponent characterIcon = JobPreferenceContainer.GetChild(); + JobPreferenceContainer.RemoveChild(characterIcon); + GameMain.Client.CharacterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) }); + + GUIListBox listBox = JobPreferenceContainer.GetChild(); /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ @@ -3432,7 +3231,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { btn.Parent.UserData = obj; - UpdateJobPreferences(listBox); + UpdateJobPreferences(); return false; }; } @@ -3443,7 +3242,7 @@ namespace Barotrauma style: "GUIButtonInfo") { UserData = jobPrefab, - OnClicked = ViewJobInfo + OnClicked = ViewJobInfo }; // Remove button @@ -3505,10 +3304,10 @@ namespace Barotrauma private GUIButton CreateJobVariantButton(Pair jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { - float relativeSize = 0.2f; + float relativeSize = 0.15f; var btn = new GUIButton(new RectTransform(new Vector2(relativeSize), slot.RectTransform, Anchor.TopCenter, scaleBasis: ScaleBasis.BothHeight) - { RelativeOffset = new Vector2(relativeSize * 1.05f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, + { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, (variantIndex + 1).ToString(), style: "JobVariantButton") { Selected = jobPrefab.Second == variantIndex, @@ -3539,7 +3338,7 @@ namespace Barotrauma .UserData as SubmarineInfo; //matching sub found and already selected, all good - if (sub != null) + if (sub != null) { if (subList == this.subList) { @@ -3578,7 +3377,7 @@ namespace Barotrauma FailedSelectedSub = null; else FailedSelectedShuttle = null; - + //hashes match, all good if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { @@ -3588,7 +3387,7 @@ namespace Barotrauma //------------------------------------------------------------------------------------- //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash - + if (subList == SubList) FailedSelectedSub = new Pair(subName, md5Hash); else @@ -3607,7 +3406,7 @@ namespace Barotrauma } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, new string[3] { sub.Name, sub.MD5Hash.ShortHash, Md5Hash.GetShortHash(md5Hash) }) + " "; } @@ -3640,7 +3439,7 @@ namespace Barotrauma { new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg); } - return false; + return false; } public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, string deliveryData) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 5ac93c54e..4430efdb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -24,7 +24,8 @@ namespace Barotrauma private GUIFrame menu; private GUIListBox serverList; - private GUIFrame serverPreview; + private GUIFrame serverPreviewContainer; + private GUIListBox serverPreview; private GUIButton joinButton; private ServerInfo selectedServer; @@ -340,11 +341,11 @@ namespace Barotrauma void RecalculateHolder() { float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; - listContainerSubtract += serverPreview.Visible ? sidebarWidth : 0.0f; + listContainerSubtract += serverPreviewContainer.Visible ? sidebarWidth : 0.0f; float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; - listContainerSubtract += serverPreviewToggleButton.Visible ? toggleButtonsSubtract : 0.0f; + listContainerSubtract += serverPreviewContainer.Visible ? toggleButtonsSubtract : 0.0f; serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); serverListHolder.Recalculate(); @@ -567,17 +568,17 @@ namespace Barotrauma { joinButton.Enabled = true; selectedServer = serverInfo; - if (!serverPreview.Visible) + if (!serverPreviewContainer.Visible) { - serverPreview.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); serverPreviewToggleButton.Visible = true; serverPreviewToggleButton.IgnoreLayoutGroups = false; - serverPreview.Visible = true; - serverPreview.IgnoreLayoutGroups = false; + serverPreviewContainer.Visible = true; + serverPreviewContainer.IgnoreLayoutGroups = false; RecalculateHolder(); } - serverInfo.CreatePreviewWindow(serverPreview); - btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + serverInfo.CreatePreviewWindow(serverPreview.Content); + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } return true; } @@ -592,24 +593,28 @@ namespace Barotrauma Visible = false, OnClicked = (btn, userdata) => { - serverPreview.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); - serverPreview.Visible = !serverPreview.Visible; - serverPreview.IgnoreLayoutGroups = !serverPreview.Visible; + serverPreviewContainer.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); + serverPreviewContainer.Visible = !serverPreviewContainer.Visible; + serverPreviewContainer.IgnoreLayoutGroups = !serverPreviewContainer.Visible; RecalculateHolder(); - btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; - serverPreview = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black, IgnoreLayoutGroups = true, Visible = false }; + serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) + { + Padding = Vector4.One * 10 * GUI.Scale + }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); @@ -1697,7 +1702,7 @@ namespace Barotrauma UpdateFriendsList(); serverList.ClearChildren(); - serverPreview.ClearChildren(); + serverPreview.Content.ClearChildren(); joinButton.Enabled = false; selectedServer = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index e40a03ece..350ff7ac5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -1604,7 +1604,7 @@ namespace Barotrauma if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } string modFolder = Path.GetDirectoryName(itemContentPackage.Path); - string filePathRelativeToModFolder = UpdaterUtil.GetRelativePath(file, Path.Combine(Environment.CurrentDirectory, modFolder)); + string filePathRelativeToModFolder = Path.GetRelativePath(Path.Combine(Environment.CurrentDirectory, modFolder), file); //file is not inside the mod folder, we need to move it if (filePathRelativeToModFolder.StartsWith("..") || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 5b4481583..e72eb2623 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -21,7 +21,7 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { - private static readonly string[] crewExperienceLevels = + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", "CrewExperienceMid", @@ -45,7 +45,7 @@ namespace Barotrauma NonLinkedGaps, TooManyLights } - + public static Vector2 MouseDragStart = Vector2.Zero; private readonly Point defaultPreviewImageSize = new Point(640, 368); @@ -100,6 +100,7 @@ namespace Barotrauma private GUIListBox previouslyUsedList; private GUIFrame undoBufferPanel; + private GUIFrame undoBufferDisclaimer; private GUIListBox undoBufferList; private GUIDropDown linkedSubBox; @@ -138,13 +139,13 @@ namespace Barotrauma //a Character used for picking up and manipulating items private Character dummyCharacter; - + /// /// Prefab used for dragging from the item catalog into inventories /// /// public static MapEntityPrefab DraggedItemPrefab; - + /// /// Currently opened hand-held item container like crates /// @@ -296,7 +297,7 @@ namespace Barotrauma }; new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", @@ -478,7 +479,10 @@ namespace Barotrauma { Visible = false }; - undoBufferList = new GUIListBox(new RectTransform(new Vector2(0.925f, 0.9f), undoBufferPanel.RectTransform, Anchor.Center)) + + Vector2 undoSize = new Vector2(0.925f, 0.9f); + + undoBufferList = new GUIListBox(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center)) { ScrollBarVisible = true, OnSelected = (_, userData) => @@ -504,11 +508,21 @@ namespace Barotrauma { Undo(amount - 1); } - + return true; } }; - + + undoBufferDisclaimer = new GUIFrame(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) + { + Color = Color.Black, + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUI.SubHeadingFont) + { + TextColor = GUI.Style.Orange + }; + UpdateUndoHistoryPanel(); //----------------------------------------------- @@ -516,9 +530,9 @@ namespace Barotrauma showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.5f), GUI.Canvas) { MinSize = new Point(190, 0) - }) - { - Visible = false + }) + { + Visible = false }; GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), showEntitiesPanel.RectTransform, Anchor.Center)) @@ -599,14 +613,14 @@ namespace Barotrauma List availableSubcategories = new List(); foreach (var prefab in MapEntityPrefab.List) { - if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) - { - availableSubcategories.Add(prefab.Subcategory); + if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) + { + availableSubcategories.Add(prefab.Subcategory); } } foreach (string subcategory in availableSubcategories) { - var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), + var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) { UserData = subcategory, @@ -643,7 +657,7 @@ namespace Barotrauma AbsoluteSpacing = (int)(GUI.Scale * 4) }; - var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), + var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => @@ -652,7 +666,7 @@ namespace Barotrauma return Item.ItemList.Count.ToString(); }; - var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), + var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => @@ -662,7 +676,7 @@ namespace Barotrauma return count.ToString(); }; - var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), + var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => @@ -670,8 +684,8 @@ namespace Barotrauma wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); return Structure.WallList.Count.ToString(); }; - - var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), + + var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); lightCountText.TextGetter = () => @@ -685,7 +699,7 @@ namespace Barotrauma lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); return lightCount.ToString(); }; - var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), + var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, wrap: true); var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); shadowCastingLightCountText.TextGetter = () => @@ -780,7 +794,7 @@ namespace Barotrauma { if (text == lastFilter) { return true; } lastFilter = text; - FilterEntities(text); + FilterEntities(text); return true; }; @@ -794,7 +808,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { OpenEntityMenu(null); - return true; + return true; } }); @@ -962,7 +976,7 @@ namespace Barotrauma #if !DEBUG if (ep.HideInMenus) { continue; } #endif - CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); + CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } entityListInner.UpdateScrollBarSize(); @@ -972,7 +986,7 @@ namespace Barotrauma entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); entityListInner.RectTransform.MinSize = new Point(0, contentHeight); - entityListInner.Content.RectTransform.SortChildren((i1, i2) => + entityListInner.Content.RectTransform.SortChildren((i1, i2) => string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); } @@ -1153,8 +1167,8 @@ namespace Barotrauma { AutoSaveInfo = XMLExtensions.TryLoadXml(autoSaveInfoPath); } - - GameMain.LightManager.AmbientLight = + + GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? new Color(3, 3, 3, 3); @@ -1219,9 +1233,9 @@ namespace Barotrauma { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } - + ImageManager.OnEditorSelected(); - + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -1293,10 +1307,10 @@ namespace Barotrauma tempTarget = DateTime.Now; wasPaused = true; } - + if (!GameMain.Instance.Paused && wasPaused) { - wasPaused = false; + wasPaused = false; target = target.AddSeconds((DateTime.Now - tempTarget).TotalSeconds); } yield return CoroutineStatus.Running; @@ -1309,7 +1323,7 @@ namespace Barotrauma } yield return CoroutineStatus.Success; } - + public override void Deselect() { base.Deselect(); @@ -1350,7 +1364,7 @@ namespace Barotrauma dummyCharacter = null; GameMain.World.ProcessChanges(); } - + GUIMessageBox.MessageBoxes.ForEachMod(component => { if (component is GUIMessageBox { Closed: false, UserData: "colorpicker" } msgBox) @@ -1363,7 +1377,7 @@ namespace Barotrauma msgBox.Close(); } }); - + ClearFilter(); } @@ -1429,8 +1443,8 @@ namespace Barotrauma min?.Remove(); } - XElement newElement = new XElement("AutoSave", - new XAttribute("file", filePath), + XElement newElement = new XElement("AutoSave", + new XAttribute("file", filePath), new XAttribute("name", Submarine.MainSub.Info.Name), new XAttribute("time", (ulong)time.TotalSeconds)); AutoSaveInfo.Root.Add(newElement); @@ -1444,7 +1458,7 @@ namespace Barotrauma DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e); } }); - + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; CrossThread.RequestExecutionOnMainThread(DisplayAutoSavePrompt); } @@ -1522,11 +1536,16 @@ namespace Barotrauma { #if DEBUG var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); + if (contentType == ContentType.OutpostModule) + { + existingFiles = existingFiles.Where(f => f.Path.Contains("Ruin") == Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin")); + } #else var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages.Where(c => c != GameMain.VanillaContent), contentType); #endif - specialSavePath = existingFiles.FirstOrDefault(f => + specialSavePath = existingFiles.FirstOrDefault(f => Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; + if (!string.IsNullOrEmpty(specialSavePath)) { specialSavePath = Path.GetDirectoryName(specialSavePath); @@ -1569,7 +1588,7 @@ namespace Barotrauma saveFrame = null; msgBox.Close(); return true; - }; + }; msgBox.Buttons[1].OnClicked = (bt, userdata) => { SaveSubToFile(nameBox.Text); @@ -1592,7 +1611,7 @@ namespace Barotrauma GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); return false; } - + foreach (var illegalChar in Path.GetInvalidFileNameChars()) { if (!name.Contains(illegalChar)) continue; @@ -1602,7 +1621,7 @@ namespace Barotrauma string savePath = name + ".sub"; string prevSavePath = null; - string directoryName = Submarine.MainSub?.Info?.FilePath == null ? + string directoryName = Submarine.MainSub?.Info?.FilePath == null ? SubmarineInfo.SavePath : Path.GetDirectoryName(Submarine.MainSub.Info.FilePath); if (!string.IsNullOrEmpty(specialSavePath)) { @@ -1721,7 +1740,7 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; Submarine.MainSub.CheckForErrors(); - + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); SubmarineInfo.RefreshSavedSub(savePath); @@ -1729,11 +1748,11 @@ namespace Barotrauma string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { if (sub.Type != SubmarineType.Player) { continue; } if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + linkedSubBox.AddItem(sub.Name, sub); } subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } @@ -1744,7 +1763,7 @@ namespace Barotrauma private void CreateSaveScreen(bool quickSave = false) { if (saveFrame != null) { return; } - + if (!quickSave) { CloseItem(); @@ -1757,7 +1776,7 @@ namespace Barotrauma }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, 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 paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1767,7 +1786,7 @@ namespace Barotrauma 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 }; - // left column ----------------------------------------------------------------------- + // left column ----------------------------------------------------------------------- var nameHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true); var saveSubLabel = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), @@ -1837,6 +1856,7 @@ namespace Barotrauma subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); foreach (SubmarineType subType in Enum.GetValues(typeof(SubmarineType))) { + if (subType == SubmarineType.Ruin) { continue; } string textTag = "SubmarineType." + subType; if (subType == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag)) { @@ -1866,13 +1886,14 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); HashSet availableFlags = new HashSet(); foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (string flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) + foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { if (flag == "none") { continue; } - availableFlags.Add(flag); + availableFlags.Add(flag); } } @@ -1881,7 +1902,7 @@ namespace Barotrauma foreach (string flag in availableFlags) { moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } if (Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) { moduleTypeDropDown.SelectItem(flag); @@ -1890,9 +1911,9 @@ namespace Barotrauma moduleTypeDropDown.OnSelected += (_, __) => { if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); + Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); moduleTypeDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", + Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width); return true; }; @@ -1903,11 +1924,11 @@ namespace Barotrauma var allowAttachGroup = 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), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft); - + var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s)) ?? "Any".ToEnumerable()), selectMultiple: true); allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); - if (Submarine.MainSub.Info.OutpostModuleInfo == null || + if (Submarine.MainSub.Info.OutpostModuleInfo == null || !Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) { @@ -2069,7 +2090,7 @@ namespace Barotrauma new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), - MinValueInt = basePrice, + MinValueInt = basePrice, MaxValueInt = 999999, OnValueChanged = (numberInput) => { @@ -2204,7 +2225,7 @@ namespace Barotrauma // right column --------------------------------------------------- new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); - + var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); @@ -2262,7 +2283,7 @@ namespace Barotrauma var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUI.SmallFont); - var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), + var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomLeft), style: "InnerFrame"); @@ -2393,9 +2414,9 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogName")); nameBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 0.1f), paddedSaveFrame.RectTransform)); @@ -2563,7 +2584,7 @@ namespace Barotrauma RelativeSpacing = 0.1f, Stretch = true }; - + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font) @@ -2659,7 +2680,7 @@ namespace Barotrauma deleteButton.Enabled = false; return true; }; - + if (AutoSaveInfo?.Root != null) { @@ -2679,11 +2700,11 @@ namespace Barotrauma DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0)); TimeSpan difference = DateTime.UtcNow - time; - string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", + string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", new[] { - "[hours]", - "[minutes]", + "[hours]", + "[minutes]", "[seconds]" }, new[] @@ -2701,7 +2722,7 @@ namespace Barotrauma if (totalMinutes < 1) { timeFormat = TextManager.Get("subeditor.savedjustnow"); - } + } else if (totalMinutes > 60) { timeFormat = TextManager.Get("subeditor.savedmorethanhour"); @@ -2710,7 +2731,7 @@ namespace Barotrauma { timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()); } - + string entryName = TextManager.GetWithVariables("subeditor.autosaveentry", new []{ "[submarine]", "[saveage]" }, new []{ submarineName, timeFormat }); loadAutoSave.AddItem(entryName, saveElement, tooltip); @@ -2759,13 +2780,13 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); - + // set the submarine file path to the "default" value loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); - try + try { - loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); + loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); } catch (Exception e) { @@ -2776,9 +2797,9 @@ namespace Barotrauma Submarine.MainSub.UpdateTransform(); Submarine.MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); - + CreateDummyCharacter(); - + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; @@ -2822,10 +2843,10 @@ namespace Barotrauma cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; - + if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { - var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), + var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); adjustLightsPrompt.Buttons[0].OnClicked += adjustLightsPrompt.Close; adjustLightsPrompt.Buttons[0].OnClicked += (btn, userdata) => @@ -2862,9 +2883,9 @@ namespace Barotrauma var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userData) => + msgBox.Buttons[0].OnClicked += (btn, userData) => { try { @@ -2880,7 +2901,7 @@ namespace Barotrauma return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; } private void OpenEntityMenu(MapEntityCategory? entityCategory) @@ -2896,12 +2917,12 @@ namespace Barotrauma } selectedCategory = entityCategory; - + SetMode(Mode.Default); saveFrame = null; loadFrame = null; - + foreach (GUIComponent child in toggleEntityMenuButton.Children) { child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; @@ -2913,15 +2934,15 @@ namespace Barotrauma var innerList = child.GetChild(); foreach (GUIComponent grandChild in innerList.Content.Children) { - grandChild.Visible = true; + grandChild.Visible = true; } } - - if (!string.IsNullOrEmpty(entityFilterBox.Text)) - { - FilterEntities(entityFilterBox.Text); + + if (!string.IsNullOrEmpty(entityFilterBox.Text)) + { + FilterEntities(entityFilterBox.Text); } - + categorizedEntityList.UpdateScrollBarSize(); categorizedEntityList.BarScroll = 0.0f; // categorizedEntityList.Visible = true; @@ -2946,7 +2967,7 @@ namespace Barotrauma } }; categorizedEntityList.UpdateScrollBarSize(); - categorizedEntityList.BarScroll = 0.0f; + categorizedEntityList.BarScroll = 0.0f; return; } @@ -2955,7 +2976,7 @@ namespace Barotrauma filter = filter.ToLower(); foreach (GUIComponent child in allEntityList.Content.Children) { - child.Visible = + child.Visible = (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); } @@ -2989,7 +3010,7 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); ClearUndoBuffer(); - + CreateDummyCharacter(); if (newMode == Mode.Wiring) { @@ -3005,14 +3026,14 @@ namespace Barotrauma dummyCharacter.Inventory.AllItems.ForEachMod(it => it.Remove()); dummyCharacter.Remove(); - dummyCharacter = null; + dummyCharacter = null; } private void CreateContextMenu() { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? + List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); @@ -3139,7 +3160,7 @@ namespace Barotrauma break; } } - + bool setValues = true; object sliderMutex = new object(), sliderTextMutex = new object(), @@ -3208,7 +3229,7 @@ namespace Barotrauma Point areaSize = new Point(rect.Width, rect.Height / 2); Rectangle newColorRect = new Rectangle(rect.Location, areaSize); Rectangle oldColorRect = new Rectangle(new Point(newColorRect.Left, newColorRect.Bottom), areaSize); - + GUI.DrawRectangle(batch, newColorRect, ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true); GUI.DrawRectangle(batch, oldColorRect, originalColor, isFilled: true); GUI.DrawRectangle(batch, rect, Color.Black, isFilled: false); @@ -3218,10 +3239,10 @@ namespace Barotrauma hueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; hueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; - + satScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; satTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; - + valueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; valueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; @@ -3251,7 +3272,7 @@ namespace Barotrauma oldProperties[color].Add(sEntity); } - List affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false }).ToList(); + List affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false } || se is ItemComponent).ToList(); StoreCommand(new PropertyCommand(affected, property.Name, newColor, oldProperties)); if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item))) @@ -3273,7 +3294,7 @@ namespace Barotrauma } return true; }; - + cancelButton.OnClicked = (button, o) => { colorPicker.DisposeTextures(); @@ -3301,7 +3322,7 @@ namespace Barotrauma SetSliderTexts(hsv); SetColorPicker(hsv); SetHex(hsv); - } + } else if (source == sliderTextMutex) { Vector3 hsv = new Vector3(hueTextBox.FloatValue * 360f, satTextBox.FloatValue, valueTextBox.FloatValue); @@ -3315,7 +3336,7 @@ namespace Barotrauma SetSliders(hsv); SetSliderTexts(hsv); SetHex(hsv); - } + } else if (source == hexMutex) { Vector3 hsv = ToolBox.RGBToHSV(XMLExtensions.ParseColor(hexValueBox.Text, errorMessages: false)); @@ -3370,7 +3391,7 @@ namespace Barotrauma static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; } - + private GUIFrame CreateWiringPanel() { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) @@ -3437,7 +3458,7 @@ namespace Barotrauma dummyCharacter.Inventory.TryPutItem(wire, slotIndex, false, false, dummyCharacter); return true; - + } /// @@ -3453,18 +3474,18 @@ namespace Barotrauma // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it oldItemPosition = itemContainer.SimPosition; TeleportDummyCharacter(oldItemPosition); - + // Override this so we can be sure the container opens var container = itemContainer.GetComponent(); if (container != null) { container.KeepOpenWhenEquipped = true; } - + // We accept any slots except "Any" since that would take priority List allowedSlots = new List(); itemContainer.AllowedSlots.ForEach(type => { if (type != InvSlotType.Any) { allowedSlots.Add(type); } }); - + // Try to place the item in the dummy character's inventory bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); if (success) { OpenedItem = itemContainer; } @@ -3540,7 +3561,7 @@ namespace Barotrauma submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; } - + private bool SelectPrefab(GUIComponent component, object obj) { allEntityList.Deselect(); @@ -3548,7 +3569,7 @@ namespace Barotrauma if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } AddPreviouslyUsed(obj as MapEntityPrefab); - + //if selecting a gap/hull/waypoint/spawnpoint, make sure the visibility is toggled on if (obj is CoreEntityPrefab prefab) { @@ -3574,14 +3595,14 @@ namespace Barotrauma { var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); var spawnedItem = false; - + itemInstance.ForEach(newItem => { if (newItem != null) { var placedItem = inv.TryPutItem(newItem, dummyCharacter); spawnedItem |= placedItem; - + if (!placedItem) { // Remove everything inside of the item so we don't get the popup asking if we want to keep the contained items @@ -3779,7 +3800,7 @@ namespace Barotrauma i--; } } - + foreach (MapEntity e in mapEntityList) { Rectangle entRect = e.WorldRect; @@ -3822,7 +3843,7 @@ namespace Barotrauma } } } - + for (int i = 0; i < hullRects.Count;) { Rectangle hullRect = hullRects[i]; @@ -3850,7 +3871,7 @@ namespace Barotrauma if (i >= hullRects.Count) break; } } - + for (int i = hullRects.Count-1; i >= 0;) { Rectangle hullRect = hullRects[i]; @@ -3878,7 +3899,7 @@ namespace Barotrauma if (i < 0) break; } } - + hullRects.Sort((a, b) => { if (a.X < b.X) return -1; @@ -3887,7 +3908,7 @@ namespace Barotrauma if (a.Y > b.Y) return 1; return 0; }); - + for (int i = 0; i < hullRects.Count - 1; i++) { Rectangle rect = hullRects[i]; @@ -3916,7 +3937,7 @@ namespace Barotrauma i--; } } - + for (int i = 0; i < hullRects.Count;i++) { Rectangle rect = hullRects[i]; @@ -3924,7 +3945,7 @@ namespace Barotrauma rect.Height += 32; hullRects[i] = rect; } - + hullRects.Sort((a, b) => { if (a.Y < b.Y) return -1; @@ -3933,7 +3954,7 @@ namespace Barotrauma if (a.X > b.X) return 1; return 0; }); - + for (int i = 0; i < hullRects.Count; i++) { for (int j = i+1; j < hullRects.Count; j++) @@ -3969,7 +3990,7 @@ namespace Barotrauma Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); } } - + public override void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } @@ -4013,7 +4034,7 @@ namespace Barotrauma saveFrame?.AddToGUIUpdateList(); } } - + /// /// GUI.MouseOn doesn't get updated while holding primary mouse and we need it to /// @@ -4023,7 +4044,7 @@ namespace Barotrauma return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) - || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false); } @@ -4087,6 +4108,8 @@ namespace Barotrauma { if (undoBufferPanel == null) { return; } + undoBufferDisclaimer.Visible = mode == Mode.Wiring; + undoBufferList.Content.Children.ForEachMod(component => { undoBufferList.Content.RemoveChild(component); @@ -4150,7 +4173,7 @@ namespace Barotrauma if (WiringMode && dummyCharacter != null) { - Wire equippedWire = + Wire equippedWire = Character.Controlled?.HeldItems.FirstOrDefault(it => it.GetComponent() != null)?.GetComponent() ?? Wire.DraggingWire; @@ -4168,9 +4191,9 @@ namespace Barotrauma } } } - + var highlightedEntities = new List(); - + // ReSharper disable once LoopCanBeConvertedToQuery foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) { @@ -4178,11 +4201,11 @@ namespace Barotrauma if (wire == null || !wire.IsMouseOn()) { continue; } highlightedEntities.Add(item); } - + MapEntity.UpdateHighlighting(highlightedEntities, true); } } - + hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; @@ -4200,11 +4223,11 @@ namespace Barotrauma else { var targetWithOffset = new Vector2(camTargetFocus.X, camTargetFocus.Y - offset / 2); - if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && + if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && Math.Abs(cam.Position.Y - targetWithOffset.Y) < 1.0f) { camTargetFocus = Vector2.Zero; - } + } else { cam.Position += (targetWithOffset - cam.Position) / cam.MoveSmoothness; @@ -4217,9 +4240,9 @@ namespace Barotrauma undoBufferList.Deselect(); } - if (GUI.KeyboardDispatcher.Subscriber == null - || MapEntity.EditingHUD != null - && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub + if (GUI.KeyboardDispatcher.Subscriber == null + || MapEntity.EditingHUD != null + && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub && MapEntity.EditingHUD.Children.Contains(sub)) { if (PlayerInput.IsCtrlDown() && !WiringMode) @@ -4229,7 +4252,7 @@ namespace Barotrauma // Ctrl+Shift+Z redos while Ctrl+Z undos if (PlayerInput.IsShiftDown()) { Redo(1); } else { Undo(1); } } - + // ctrl+Y redo if (PlayerInput.KeyHit(Keys.Y)) { @@ -4288,7 +4311,7 @@ namespace Barotrauma } } } - + // Focus to selection if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) { @@ -4310,7 +4333,7 @@ namespace Barotrauma camTargetFocus = rect.Center.ToVector2(); } } - + if (GameMain.Config.KeyBind(InputType.ToggleInventory).IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); @@ -4396,7 +4419,7 @@ namespace Barotrauma lightComponent.LightColor; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } - } + } GameMain.LightManager?.Update((float)deltaTime); } @@ -4432,7 +4455,7 @@ namespace Barotrauma }); } - if (dummyCharacter.SelectedConstruction == null || + if (dummyCharacter.SelectedConstruction == null || dummyCharacter.SelectedConstruction.GetComponent() != null) { if (WiringMode && PlayerInput.IsShiftDown()) @@ -4449,12 +4472,12 @@ namespace Barotrauma var (cursorX, cursorY) = dummyCharacter.CursorPosition; bool isHorizontal = Math.Abs(cursorX - lastNode.X) < Math.Abs(cursorY - lastNode.Y); - + float roundedY = MathUtils.Round(cursorY, Submarine.GridSize.Y / 2.0f); float roundedX = MathUtils.Round(cursorX, Submarine.GridSize.X / 2.0f); - dummyCharacter.CursorPosition = isHorizontal - ? new Vector2(lastNode.X, roundedY) + dummyCharacter.CursorPosition = isHorizontal + ? new Vector2(lastNode.X, roundedY) : new Vector2(roundedX, lastNode.Y); } } @@ -4464,7 +4487,7 @@ namespace Barotrauma { TeleportDummyCharacter(oldItemPosition); } - + if (WiringMode && dummyCharacter?.SelectedConstruction == null) { TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition)); @@ -4486,31 +4509,31 @@ namespace Barotrauma if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) { var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; - + // So we don't accidentally drag inventory items while doing this if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); } - - switch (DraggedItemPrefab) + + switch (DraggedItemPrefab) { // regular item prefabs - case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: { bool spawnedItem = false; for (var i = 0; i < inv.Capacity; i++) { var slot = inv.visualSlots[i]; var itemContainer = inv.GetItemAt(i)?.GetComponent(); - + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit if (Inventory.IsMouseOnSlot(slot)) { var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); - + if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); spawnedItem |= placedItem; - + if (!placedItem) { newItem.Remove(); @@ -4520,7 +4543,7 @@ namespace Barotrauma { bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); spawnedItem |= placedItem; - + // try to place the item into the inventory of the item we are hovering over if (!placedItem) { @@ -4564,28 +4587,28 @@ namespace Barotrauma { // load the items var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - + // counter for items that failed so we so we known that slot remained empty var failedCount = 0; - + for (var j = 0; j < itemInstance.Count(); j++) { var newItem = itemInstance[j]; var newSpot = i + j - failedCount; - + // try to find a valid slot to put the items - while (inv.visualSlots.Length > newSpot) + while (inv.visualSlots.Length > newSpot) { if (inv.GetItemAt(newSpot) == null) { break; } newSpot++; } - + // valid slot found if (inv.visualSlots.Length > newSpot) { var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); spawnedItems |= placedItem; - + if (!placedItem) { failedCount++; @@ -4598,7 +4621,7 @@ namespace Barotrauma { var placedItem = inv.TryPutItem(newItem, dummyCharacter); spawnedItems |= placedItem; - + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort if (!placedItem) { @@ -4664,11 +4687,11 @@ namespace Barotrauma MeasurePositionStart = cam.ScreenToWorld(PlayerInput.MousePosition); } } - + if (!WiringMode) { bool shouldCloseHud = dummyCharacter?.SelectedConstruction != null && HUD.CloseHUD(dummyCharacter.SelectedConstruction.Rect) && DraggedItemPrefab == null; - + if (MapEntityPrefab.Selected != null && GUI.MouseOn == null) { MapEntityPrefab.Selected.UpdatePlacing(cam); @@ -4685,7 +4708,7 @@ namespace Barotrauma { if (dummyCharacter?.SelectedConstruction == null) { - CreateContextMenu(); + CreateContextMenu(); } DraggedItemPrefab = null; } @@ -4695,11 +4718,11 @@ namespace Barotrauma { CloseItem(); } - } + } MapEntity.UpdateEditor(cam, (float)deltaTime); } - entityMenuOpenState = entityMenuOpen && !WiringMode ? + entityMenuOpenState = entityMenuOpen && !WiringMode ? (float)Math.Min(entityMenuOpenState + deltaTime * 5.0f, 1.0f) : (float)Math.Max(entityMenuOpenState - deltaTime * 5.0f, 0.0f); @@ -4723,7 +4746,7 @@ namespace Barotrauma { saveFrame = null; } - } + } if (dummyCharacter != null) { @@ -4789,28 +4812,28 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } - Submarine.DrawBack(spriteBatch, true, e => - e is Structure s && - !IsSubcategoryHidden(e.prefab?.Subcategory) && + Submarine.DrawBack(spriteBatch, true, e => + e is Structure s && + !IsSubcategoryHidden(e.prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - + // When we "open" a wearable item with inventory it won't get rendered because the dummy character is invisible // So we are drawing a clone of it on the same position if (OpenedItem?.GetComponent() != null) { - OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), + OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), scale: OpenedItem.Scale, color: OpenedItem.SpriteColor, depth: OpenedItem.SpriteDepth); GUI.DrawRectangle(spriteBatch, new Vector2(OpenedItem.WorldRect.X, -OpenedItem.WorldRect.Y), new Vector2(OpenedItem.Rect.Width, OpenedItem.Rect.Height), Color.White, false, 0, (int)Math.Max(2.0f / cam.Zoom, 1.0f)); } - - Submarine.DrawBack(spriteBatch, true, e => + + Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); @@ -4876,7 +4899,7 @@ namespace Barotrauma } } } - + if (dummyCharacter != null) { if (WiringMode) @@ -4913,7 +4936,7 @@ namespace Barotrauma Vector2 offset = new Vector2(GUI.IntScale(24)); GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUI.Style.TextColor, font: GUI.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); } - + spriteBatch.End(); } @@ -4950,13 +4973,13 @@ namespace Barotrauma Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); - + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); rt.SaveAsPng(stream, width, height); } - //for some reason setting the rendertarget changes the size of the viewport + //for some reason setting the rendertarget changes the size of the viewport //but it doesn't change back to default when setting it back to null GameMain.Instance.ResetViewPort(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs new file mode 100644 index 000000000..38b34c0a0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -0,0 +1,112 @@ +#nullable enable +using System; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +/* + * This screen only exists because I'm going mental without access to EnC on Linux. + * This is fucking stupid and horrible. + * Remember to remove this crap eventually. + * - Markus + */ +namespace Barotrauma +{ + class TestScreen : EditorScreen + { + public override Camera Cam { get; } + + private Item? miniMapItem; + + private Submarine? submarine; + private Character? dummyCharacter; + public static Effect BlueprintEffect; + + private TabMenu tabMenu; + + public TestScreen() + { + Cam = new Camera(); + BlueprintEffect = GameMain.GameScreen.BlueprintEffect; + + new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") + { + OnClicked = (button, o) => + { + BlueprintEffect.Dispose(); + GameMain.Instance.Content.Unload(); + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + GameMain.GameScreen.BlueprintEffect = BlueprintEffect; + return true; + } + }; + } + + public override void Select() + { + base.Select(); + + if (dummyCharacter is { Removed: false }) + { + dummyCharacter?.Remove(); + } + + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom()); + dummyCharacter.Info.Name = "Galldren"; + dummyCharacter.Inventory.CreateSlots(); + + Character.Controlled = dummyCharacter; + GameMain.World.ProcessChanges(); + TabMenu.selectedTab = TabMenu.InfoFrameTab.Talents; + tabMenu = new TabMenu(); + } + + public override void AddToGUIUpdateList() + { + Frame.AddToGUIUpdateList(); + CharacterHUD.AddToGUIUpdateList(dummyCharacter); + dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); + tabMenu.AddToGUIUpdateList(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + tabMenu.Update(); + + if (dummyCharacter is { } dummy) + { + dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + dummy.Control((float)deltaTime, Cam); + } + + GUI.Update((float)deltaTime); + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + base.Draw(deltaTime, graphics, spriteBatch); + graphics.Clear(BackgroundColor); + + spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: Cam.Transform); + miniMapItem?.Draw(spriteBatch, false); + if (dummyCharacter is { } dummy) + { + dummyCharacter.DrawFront(spriteBatch, Cam); + dummyCharacter.Draw(spriteBatch, Cam); + } + spriteBatch.End(); + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); + + GUI.Draw(Cam, spriteBatch); + + dummyCharacter?.DrawHUD(spriteBatch, Cam, false); + + spriteBatch.End(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 986afef6d..1fc36bc42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -271,6 +271,21 @@ namespace Barotrauma } } } + else if (newValue is string[] a) + { + for (int i = 0; i < fields.Length; i++) + { + if (i >= a.Length) { break; } + if (fields[i] is GUITextBox textBox) + { + textBox.Text = a[i]; + if (flash) + { + textBox.Flash(GUI.Style.Green); + } + } + } + } } public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) @@ -423,6 +438,10 @@ namespace Barotrauma { propertyField = CreateRectangleField(entity, property, r, displayName, toolTip); } + else if(value is string[] a) + { + propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip); + } return propertyField; } @@ -1164,6 +1183,75 @@ namespace Barotrauma return frame; } + public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, string displayName, string toolTip) + { + int elementCount = (value.Length + 1); + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementCount * elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUI.SmallFont) + { + ToolTip = toolTip + }; + var editableAttribute = property.GetAttribute(); + var fields = new GUIComponent[value.Length]; + var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, (float)(elementCount - 1) / elementCount), frame.RectTransform, anchor: Anchor.BottomLeft)) + { + RelativeSpacing = 0.01f + }; + elementCount -= 1; + + for (int i = 0; i < value.Length; i++) + { + var element = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point((int)(0.9f * inputArea.Rect.Width), 50) }, style: null); + var elementLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, element.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + // 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: GUI.SmallFont, textAlignment: Alignment.Center); + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUI.SmallFont }; + int comp = i; + textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); + textBox.OnDeselected += (textBox, keys) => OnApply(textBox); + fields[i] = textBox; + + bool OnApply(GUITextBox textBox) + { + // Reserve the semicolon for serializing the value + bool containsForbiddenCharacters = textBox.Text.Contains(';'); + string[] newValue = (string[])property.GetValue(entity); + if (!containsForbiddenCharacters) + { + newValue[comp] = textBox.Text; + if (SetPropertyValue(property, entity, newValue)) + { + TrySendNetworkUpdate(entity, property); + textBox.Flash(color: GUI.Style.Green, flashDuration: 1f); + } + } + else + { + textBox.Text = newValue[comp]; + textBox.Flash(color: GUI.Style.Red, flashDuration: 1f); + } + return true; + } + } + + refresh += () => + { + if (fields.None(f => ((GUITextBox)f).Selected)) + { + string[] value = (string[])property.GetValue(entity); + for (int i = 0; i < fields.Length; i++) + { + ((GUITextBox)fields[i]).Text = value[i]; + } + } + }; + + frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Sum(c => c.MinSize.Y)); + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + return frame; + } + public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index de7051846..172454cc2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -38,6 +38,7 @@ namespace Barotrauma public readonly string File; public readonly string Type; public readonly bool DuckVolume; + public readonly float Volume; public readonly Vector2 IntensityRange; @@ -52,6 +53,7 @@ namespace Barotrauma this.Type = element.GetAttributeString("type", "").ToLowerInvariant(); this.IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); this.DuckVolume = element.GetAttributeBool("duckvolume", false); + this.Volume = element.GetAttributeFloat("volume", 1.0f); this.ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); this.Element = element; } @@ -816,6 +818,8 @@ namespace Barotrauma } } + int noiseLoopIndex = 1; + updateMusicTimer -= deltaTime; if (updateMusicTimer <= 0.0f) { @@ -825,7 +829,6 @@ namespace Barotrauma GameMain.GameSession.EventManager.CurrentIntensity * 100.0f : 0.0f; IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); - int mainTrackIndex = 0; if (suitableMusic.Count() == 0) { @@ -851,14 +854,12 @@ namespace Barotrauma } } - int noiseLoopIndex = 1; if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips(Level.Loaded.LevelData?.Biome?.Identifier, currentIntensity) : Enumerable.Empty(); - if (suitableNoiseLoops.Count() == 0) { targetMusic[noiseLoopIndex] = null; @@ -874,12 +875,23 @@ namespace Barotrauma targetMusic[noiseLoopIndex] = null; } + IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience", currentIntensity); + int typeAmbienceTrackIndex = 2; + if (suitableTypeAmbiences.None()) + { + targetMusic[typeAmbienceTrackIndex] = null; + } + // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore + else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m.File == currentMusic[typeAmbienceTrackIndex].Filename)) + { + targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandom(); + } + //get the appropriate intensity layers for current situation IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips("intensity", currentIntensity) : Enumerable.Empty(); - - int intensityTrackStartIndex = 2; + int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore @@ -888,7 +900,6 @@ namespace Barotrauma targetMusic[i] = null; } } - foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing @@ -917,7 +928,7 @@ namespace Barotrauma { //mute the channel musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); - if (musicChannel[i].Gain < 0.01f) DisposeMusicChannel(i); + if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } } //something should be playing, but the targetMusic is invalid @@ -932,7 +943,7 @@ namespace Barotrauma if (musicChannel[i] != null && musicChannel[i].IsPlaying) { musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, 0.0f, MusicLerpSpeed * deltaTime); - if (musicChannel[i].Gain < 0.01f) DisposeMusicChannel(i); + if (musicChannel[i].Gain < 0.01f) { DisposeMusicChannel(i); } } //channel free now, start playing the correct clip if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) @@ -949,7 +960,7 @@ namespace Barotrauma targetMusic[i] = null; break; } - musicChannel[i] = currentMusic[i].Play(0.0f, "music"); + musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "" : "music"); if (targetMusic[i].ContinueFromPreviousTime) { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; @@ -963,13 +974,13 @@ namespace Barotrauma if (musicChannel[i] == null || !musicChannel[i].IsPlaying) { musicChannel[i]?.Dispose(); - musicChannel[i] = currentMusic[i].Play(0.0f, "music"); + musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "" : "music"); musicChannel[i].Looping = true; } - float targetGain = 1.0f; + float targetGain = targetMusic[i].Volume; if (targetMusic[i].DuckVolume) { - targetGain = (float)Math.Sqrt(1.0f / activeTrackCount); + targetGain *= (float)Math.Sqrt(1.0f / activeTrackCount); } musicChannel[i].Gain = MathHelper.Lerp(musicChannel[i].Gain, targetGain, MusicLerpSpeed * deltaTime); } @@ -1009,7 +1020,8 @@ namespace Barotrauma Screen.Selected == GameMain.SpriteEditorScreen || Screen.Selected == GameMain.SubEditorScreen || Screen.Selected == GameMain.EventEditorScreen || - (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode)) + (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode) || + Screen.Selected == GameMain.NetLobbyScreen) { return "editor"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 143a70d25..ed81e93a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -41,21 +41,13 @@ namespace Barotrauma public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f, string path = null) { this.texture = texture; - sourceRect = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height); - offset = newOffset ?? Vector2.Zero; - size = new Vector2(sourceRect.Width, sourceRect.Height); - origin = Vector2.Zero; - effects = SpriteEffects.None; - rotation = newRotation; - FilePath = path; - AddToList(this); } @@ -85,7 +77,7 @@ namespace Barotrauma EnsureLazyLoaded(isAsync: true); } - public void EnsureLazyLoaded(bool isAsync=false) + public void EnsureLazyLoaded(bool isAsync = false) { if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; } loadingAsync = isAsync; @@ -177,7 +169,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Sprite \"" + file + "\" not found!"); + DebugConsole.ThrowError($"Sprite \"{file}\" not found! {Environment.StackTrace.CleanupStackTrace()}"); } return null; @@ -382,7 +374,7 @@ namespace Barotrauma { foreach (Sprite s in LoadedSprites) { - if (s.FullPath == FullPath) return; + if (s.FullPath == FullPath) { return; } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 403a70475..6327b9e26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -110,14 +110,14 @@ namespace Barotrauma foreach (StatusEffect statusEffect in successEffects) { float duration = statusEffect.Duration; - onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } foreach (StatusEffect statusEffect in failureEffects) { float duration = statusEffect.Duration; - onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index d5847be30..327dac75a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -1,9 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; -using System.Text.RegularExpressions; +using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma { @@ -54,6 +55,225 @@ namespace Barotrauma return isInside; } + + public static Vector2 GetPolygonBoundingBoxSize(List verticess) + { + float minX = verticess[0].X; + float maxX = verticess[0].X; + float minY = verticess[0].Y; + float maxY = verticess[0].Y; + + foreach (var (vertX, vertY) in verticess) + { + minX = Math.Min(vertX, minX); + maxX = Math.Max(vertX, maxX); + minY = Math.Min(vertY, minY); + maxY = Math.Max(vertY, maxY); + } + + return new Vector2(maxX - minX, maxY - minY); + } + + public static List ScalePolygon(List vertices, Vector2 scale) + { + List newVertices = new List(); + + Vector2 center = GetPolygonCentroid(vertices); + + foreach (Vector2 vert in vertices) + { + Vector2 centerVector = vert - center; + Vector2 centerVectorScale = centerVector * scale; + Vector2 scaledVector = centerVectorScale + center; + newVertices.Add(scaledVector); + } + + return newVertices; + } + + public static Vector2 GetPolygonCentroid(List poly) + { + float accumulatedArea = 0.0f; + float centerX = 0.0f; + float centerY = 0.0f; + + for (int i = 0, j = poly.Count - 1; i < poly.Count; j = i++) + { + float temp = poly[i].X * poly[j].Y - poly[j].X * poly[i].Y; + accumulatedArea += temp; + centerX += (poly[i].X + poly[j].X) * temp; + centerY += (poly[i].Y + poly[j].Y) * temp; + } + + if (Math.Abs(accumulatedArea) < 1E-7f) { return Vector2.Zero; } // Avoid division by zero + + accumulatedArea *= 3f; + return new Vector2(centerX / accumulatedArea, centerY / accumulatedArea); + } + + public static List SnapVertices(List points, int treshold = 1) + { + Stack toCheck = new Stack(); + List newPoints = new List(); + + foreach (Vector2 point in points) + { + toCheck.Push(point); + } + + while (toCheck.TryPop(out Vector2 point)) + { + Vector2 newPoint = new Vector2(point.X, point.Y); + foreach (Vector2 otherPoint in toCheck.Concat(newPoints)) + { + float diffX = Math.Abs(newPoint.X - otherPoint.X), + diffY = Math.Abs(newPoint.Y - otherPoint.Y); + + if (diffX <= treshold) + { + newPoint.X = Math.Max(newPoint.X, otherPoint.X); + } + + if (diffY <= treshold) + { + newPoint.Y = Math.Max(newPoint.Y, otherPoint.Y); + } + } + newPoints.Add(newPoint); + } + + return newPoints; + } + + public static ImmutableArray SnapRectangles(IEnumerable rects, int treshold = 1) + { + List list = new List(); + + List points = new List(); + + foreach (RectangleF rect in rects) + { + points.Add(new Vector2(rect.Left, rect.Top)); + points.Add(new Vector2(rect.Right, rect.Top)); + points.Add(new Vector2(rect.Right, rect.Bottom)); + points.Add(new Vector2(rect.Left, rect.Bottom)); + } + + points = SnapVertices(points, treshold); + + for (int i = 0; i < points.Count; i += 4) + { + Vector2 topLeft = points[i]; + Vector2 bottomRight = points[i + 2]; + + list.Add(new RectangleF(topLeft, bottomRight - topLeft)); + } + + return list.ToImmutableArray(); + } + + public static List> CombineRectanglesIntoShape(IEnumerable rectangles) + { + List points = + (from point in rectangles.SelectMany(RectangleToPoints) + group point by point + into g + where g.Count() % 2 == 1 + select g.Key) + .ToList(); + + List sortedY = points.OrderBy(p => p.Y).ThenByDescending(p => p.X).ToList(); + List sortedX = points.OrderBy(p => p.X).ThenByDescending(p => p.Y).ToList(); + + Dictionary edgesH = new Dictionary(); + Dictionary edgesV = new Dictionary(); + + int i = 0; + while (i < points.Count) + { + float currY = sortedY[i].Y; + + while (i < points.Count && Math.Abs(sortedY[i].Y - currY) < 0.01f) + { + edgesH[sortedY[i]] = sortedY[i + 1]; + edgesH[sortedY[i + 1]] = sortedY[i]; + i += 2; + } + + } + + i = 0; + + while (i < points.Count) + { + float currX = sortedX[i].X; + while (i < points.Count && Math.Abs(sortedX[i].X - currX) < 0.01f) + { + edgesV[sortedX[i]] = sortedX[i + 1]; + edgesV[sortedX[i + 1]] = sortedX[i]; + i += 2; + } + } + + List> polygons = new List>(); + + while (edgesH.Any()) + { + var (key, _) = edgesH.First(); + List<(Vector2 Point, int Direction)> polygon = new List<(Vector2 Point, int Direction)> { (key, 0) }; + edgesH.Remove(key); + + while (true) + { + var (curr, direction) = polygon[^1]; + + if (direction == 0) + { + Vector2 nextVertex = edgesV[curr]; + edgesV.Remove(curr); + polygon.Add((nextVertex, 1)); + } + else + { + Vector2 nextVertex = edgesH[curr]; + edgesH.Remove(curr); + polygon.Add((nextVertex, 0)); + } + + if (polygon[^1] == polygon[0]) + { + polygon.Remove(polygon[^1]); + break; + } + } + + List poly = polygon.Select(t => t.Point).ToList(); + + foreach (Vector2 vertex in poly) + { + if (edgesH.ContainsKey(vertex)) + { + edgesH.Remove(vertex); + } + + if (edgesV.ContainsKey(vertex)) + { + edgesV.Remove(vertex); + } + } + + polygons.Add(poly); + } + + return polygons; + + static IEnumerable RectangleToPoints(RectangleF rect) + { + (float x1, float y1, float x2, float y2) = (rect.Left, rect.Top, rect.Right, rect.Bottom); + Vector2[] pts = { new Vector2(x1, y1), new Vector2(x2, y1), new Vector2(x2, y2), new Vector2(x1, y2) }; + return pts; + } + } // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(this Color color) diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb new file mode 100644 index 000000000..8ef444671 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb new file mode 100644 index 000000000..608cdb04c Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/blueprintshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb new file mode 100644 index 000000000..e9784b881 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb new file mode 100644 index 000000000..9a7b8c92d Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 288e24afa..1af334808 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -36,6 +36,7 @@ TRACE;CLIENT;LINUX;USE_STEAM;UNSTABLE x64 ..\bin\$(Configuration)Linux\ + true @@ -48,6 +49,7 @@ TRACE;CLIENT;LINUX;X64;USE_STEAM;UNSTABLE x64 ..\bin\$(Configuration)Linux\ + true @@ -121,7 +123,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index a75b63442..194cf61c6 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -38,7 +38,8 @@ TRACE;CLIENT;OSX;USE_STEAM;RELEASE;NETCOREAPP;NETCOREAPP3_0;UNSTABLE x64 - ..\bin\$(Configuration)Mac + ..\bin\$(Configuration)Mac + true @@ -51,6 +52,7 @@ TRACE;CLIENT;OSX;X64;USE_STEAM;UNSTABLE x64 ..\bin\$(Configuration)Mac\ + true @@ -122,7 +124,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb index 901a2171c..7d48e26be 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb @@ -67,3 +67,15 @@ /processorParam:DebugMode=Auto /build:grainshader.fx +#begin thresholdtint.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:thresholdtint.fx + +#begin blueprintshader.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:blueprintshader.fx + diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index cb119ed14..82d54dedf 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -67,3 +67,14 @@ /processorParam:DebugMode=Auto /build:grainshader_opengl.fx +#begin blueprintshader_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:blueprintshader_opengl.fx + +#begin thresholdtint_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:thresholdtint_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx b/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx new file mode 100644 index 000000000..87048562e --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/blueprintshader.fx @@ -0,0 +1,48 @@ +// vim:ft=hlsl +sampler TextureSampler : register(s0); + +float width; +float height; + +float3 sobel(float2 uv) +{ + float x = 0; + float y = 0; + + float w = 1.0 / width; + float h = 1.0 / height; + + x += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + x += tex2D(TextureSampler, uv + float2(-w, 0)) * -2.0; + x += tex2D(TextureSampler, uv + float2(-w, h)) * -1.0; + + x += tex2D(TextureSampler, uv + float2( w, -h)) * 1.0; + x += tex2D(TextureSampler, uv + float2( w, 0)) * 2.0; + x += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + y += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + y += tex2D(TextureSampler, uv + float2( 0, -h)) * -2.0; + y += tex2D(TextureSampler, uv + float2( w, -h)) * -1.0; + + y += tex2D(TextureSampler, uv + float2(-w, h)) * 1.0; + y += tex2D(TextureSampler, uv + float2( 0, h)) * 2.0; + y += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + return sqrt(x * x + y * y); +} + +float4 blueprint(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float3 s = sobel(texCoord); + float a = tex2D(TextureSampler, texCoord).a; + a *= clr.a; + return float4(clr.r + s.r, clr.g + s.g, clr.b + s.b, a); +} + +technique Blueprint +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 blueprint(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx new file mode 100644 index 000000000..4af0d4b36 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/blueprintshader_opengl.fx @@ -0,0 +1,48 @@ +// vim:ft=hlsl +sampler TextureSampler : register(s0); + +float width; +float height; + +float3 sobel(float2 uv) +{ + float x = 0; + float y = 0; + + float w = 1.0 / width; + float h = 1.0 / height; + + x += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + x += tex2D(TextureSampler, uv + float2(-w, 0)) * -2.0; + x += tex2D(TextureSampler, uv + float2(-w, h)) * -1.0; + + x += tex2D(TextureSampler, uv + float2( w, -h)) * 1.0; + x += tex2D(TextureSampler, uv + float2( w, 0)) * 2.0; + x += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + y += tex2D(TextureSampler, uv + float2(-w, -h)) * -1.0; + y += tex2D(TextureSampler, uv + float2( 0, -h)) * -2.0; + y += tex2D(TextureSampler, uv + float2( w, -h)) * -1.0; + + y += tex2D(TextureSampler, uv + float2(-w, h)) * 1.0; + y += tex2D(TextureSampler, uv + float2( 0, h)) * 2.0; + y += tex2D(TextureSampler, uv + float2( w, h)) * 1.0; + + return sqrt(x * x + y * y); +} + +float4 blueprint(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float3 s = sobel(texCoord); + float a = tex2D(TextureSampler, texCoord).a; + a *= clr.a; + return float4(clr.r + s.r, clr.g + s.g, clr.b + s.b, a); +} + +technique Blueprint +{ + pass Pass1 + { + PixelShader = compile ps_3_0 blueprint(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx b/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx new file mode 100644 index 000000000..b70d04055 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx @@ -0,0 +1,32 @@ +Texture2D xBaseTexture; +sampler BaseTextureSampler = sampler_state { Texture = ; }; +Texture2D xTintMaskTexture; +sampler TintMaskTextureSampler = sampler_state { Texture = ; }; +Texture2D xCutoffTexture; +sampler CutoffTextureSampler = sampler_state { Texture = ; }; + +float highlightThreshold; +float highlightMultiplier; + +float baseToCutoffSizeRatio; + +float4 mainPS(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 baseSample = xBaseTexture.Sample(BaseTextureSampler, texCoord); + float3 tintMaskSample = xTintMaskTexture.Sample(TintMaskTextureSampler, texCoord).rgb; + float cutoffSample = xCutoffTexture.Sample(CutoffTextureSampler, texCoord * baseToCutoffSizeRatio).r; + + float3 highlight = saturate((baseSample.rgb - (highlightThreshold * float3(1,1,1))) * highlightMultiplier); + float3 tinted = saturate(baseSample.rgb * clr.rgb + highlight); + return float4( + (tinted * tintMaskSample) + (baseSample.rgb * (float3(1,1,1) - tintMaskSample)), + baseSample.a * cutoffSample * clr.a); +} + +technique ThresholdTintShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 mainPS(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx new file mode 100644 index 000000000..ff693a035 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx @@ -0,0 +1,32 @@ +Texture2D xBaseTexture; +sampler BaseTextureSampler = sampler_state { Texture = ; }; +Texture2D xTintMaskTexture; +sampler TintMaskTextureSampler = sampler_state { Texture = ; }; +Texture2D xCutoffTexture; +sampler CutoffTextureSampler = sampler_state { Texture = ; }; + +float highlightThreshold; +float highlightMultiplier; + +float baseToCutoffSizeRatio; + +float4 mainPS(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 baseSample = tex2D(BaseTextureSampler, texCoord); + float3 tintMaskSample = tex2D(TintMaskTextureSampler, texCoord).rgb; + float cutoffSample = tex2D(CutoffTextureSampler, texCoord * baseToCutoffSizeRatio).r; + + float3 highlight = saturate((baseSample.rgb - (highlightThreshold * float3(1,1,1))) * highlightMultiplier); + float3 tinted = saturate(baseSample.rgb * clr.rgb + highlight); + return float4( + (tinted * tintMaskSample) + (baseSample.rgb * (float3(1,1,1) - tintMaskSample)), + baseSample.a * cutoffSample * clr.a); +} + +technique ThresholdTintShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 mainPS(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 6402c5ba2..8fea287a3 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma @@ -40,6 +40,7 @@ TRACE;CLIENT;WINDOWS;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + true @@ -56,6 +57,7 @@ ..\bin\$(Configuration)Windows\ full true + true @@ -125,7 +127,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c1df82ea9..425e1f28a 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -36,6 +36,7 @@ TRACE;SERVER;LINUX;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + true @@ -48,6 +49,7 @@ TRACE;SERVER;LINUX;X64;USE_STEAM x64 ..\bin\$(Configuration)Linux\ + true diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 0212730ea..1dd8c8d75 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -41,6 +41,7 @@ x64 ..\bin\ReleaseMac + true @@ -53,6 +54,7 @@ TRACE;SERVER;OSX;X64;USE_STEAM;UNSTABLE x64 ..\bin\$(Configuration)Mac\ + true diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index f605e430a..653db39e1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -28,6 +28,16 @@ namespace Barotrauma } } GameMain.Lua.hook.Call("characterDeath", new object[] { this,causeOfDeathAffliction }); + + if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter)) + { + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); + if (ownerClient != null) + { + (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient); + } + } + healthUpdateTimer = 0.0f; if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this) @@ -49,5 +59,10 @@ namespace Barotrauma } } } + + partial void OnMoneyChanged(int prevAmount, int newAmount) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index f4d7baa52..81abdaf26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -9,8 +10,9 @@ namespace Barotrauma { private readonly Dictionary prevSentSkill = new Dictionary(); - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) + partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel) { + if (Character == null || Character.Removed) { return; } if (!prevSentSkill.ContainsKey(skillIdentifier)) { prevSentSkill[skillIdentifier] = prevLevel; @@ -22,6 +24,21 @@ namespace Barotrauma } } + partial void OnExperienceChanged(int prevAmount, int newAmount) + { + if (Character == null || Character.Removed) { return; } + if (prevAmount != newAmount) + { + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateExperience }); + } + } + + partial void OnPermanentStatChanged(StatTypes statType) + { + if (Character == null || Character.Removed) { return; } + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statType }); + } + public void ServerWrite(IWriteMessage msg) { msg.Write(ID); @@ -30,10 +47,13 @@ namespace Barotrauma msg.Write((byte)Gender); msg.Write((byte)Race); msg.Write((byte)HeadSpriteId); - msg.Write((byte)Head.HairIndex); - msg.Write((byte)Head.BeardIndex); - msg.Write((byte)Head.MoustacheIndex); - msg.Write((byte)Head.FaceAttachmentIndex); + msg.Write((byte)HairIndex); + msg.Write((byte)BeardIndex); + msg.Write((byte)MoustacheIndex); + msg.Write((byte)FaceAttachmentIndex); + msg.WriteColorR8G8B8(SkinColor); + msg.WriteColorR8G8B8(HairColor); + msg.WriteColorR8G8B8(FacialHairColor); msg.Write(ragdollFileName); if (Job != null) @@ -53,6 +73,19 @@ namespace Barotrauma msg.Write((byte)0); } // TODO: animations + msg.Write((byte)SavedStatValues.SelectMany(s => s.Value).Count()); + foreach (var savedStatValuePair in SavedStatValues) + { + foreach (var savedStatValue in savedStatValuePair.Value) + { + msg.Write((byte)savedStatValuePair.Key); + msg.Write(savedStatValue.StatIdentifier); + msg.Write(savedStatValue.StatValue); + msg.Write(savedStatValue.RemoveOnDeath); + } + } + msg.Write((ushort)ExperiencePoints); + msg.Write((ushort)AdditionalTalentPoints); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index bcd25ddf9..bc7428d42 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -238,7 +238,7 @@ namespace Barotrauma break; case ClientNetObject.ENTITY_STATE: - int eventType = msg.ReadRangedInteger(0, 3); + int eventType = msg.ReadRangedInteger(0, 4); switch (eventType) { case 0: @@ -268,8 +268,35 @@ namespace Barotrauma if (IsIncapacitated) { var causeOfDeath = CharacterHealth.GetCauseOfDeath(); - Kill(causeOfDeath.First, causeOfDeath.Second); + Kill(causeOfDeath.type, causeOfDeath.affliction); } + break; + case 3: // NetEntityEvent.Type.UpdateTalents + if (c.Character != this) + { +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); +#endif + return; + } + + // get the full list of talents from the player, only give the ones + // that are not already given (or otherwise not viable) + ushort talentCount = msg.ReadUInt16(); + List talentSelection = new List(); + for (int i = 0; i < talentCount; i++) + { + UInt32 talentIdentifier = msg.ReadUInt32(); + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); + if (prefab != null) { talentSelection.Add(prefab.Identifier); } + } + talentSelection = TalentTree.CheckTalentSelection(this, talentSelection); + + foreach (string talent in talentSelection) + { + GiveTalent(talent); + } + break; } break; @@ -283,7 +310,7 @@ namespace Barotrauma if (extraData != null) { - const int min = 0, max = 9; + const int min = 0, max = 13; switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: @@ -394,6 +421,47 @@ namespace Barotrauma msg.Write(inventoryItemIDs[i]); } break; + case NetEntityEvent.Type.UpdateExperience: + msg.WriteRangedInteger(10, min, max); + msg.Write(Info.ExperiencePoints); + break; + case NetEntityEvent.Type.UpdateTalents: + msg.WriteRangedInteger(11, min, max); + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.AddedThisRound); + msg.Write(unlockedTalent.Prefab.UIntIdentifier); + } + break; + case NetEntityEvent.Type.UpdateMoney: + msg.WriteRangedInteger(12, min, max); + msg.Write(GameMain.GameSession.Campaign.Money); + break; + case NetEntityEvent.Type.UpdatePermanentStats: + msg.WriteRangedInteger(13, min, max); + if (Info == null || extraData.Length < 2 || !(extraData[1] is StatTypes statType)) + { + msg.Write((byte)0); + msg.Write((byte)0); + } + else if (!Info.SavedStatValues.ContainsKey(statType)) + { + msg.Write((byte)0); + msg.Write((byte)statType); + } + else + { + msg.Write((byte)Info.SavedStatValues[statType].Count); + msg.Write((byte)statType); + foreach (var savedStatValue in Info.SavedStatValues[statType]) + { + msg.Write(savedStatValue.StatIdentifier); + msg.Write(savedStatValue.StatValue); + msg.Write(savedStatValue.RemoveOnDeath); + } + } + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -499,7 +567,7 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); - (AIController as EnemyAIController)?.PetBehavior?.ServerWrite(tempBuffer); + AIController?.ServerWrite(tempBuffer); HealthUpdatePending = false; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a3846fb7e..308dbf1c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1187,6 +1187,7 @@ namespace Barotrauma NewMessage("*****************", Color.Lime); GameServer.Log("Console command \"restart\" executed: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.Instance.CloseServer(); + GameMain.Instance.TryStartChildServerRelay(); GameMain.Instance.StartServer(); })); @@ -1692,10 +1693,25 @@ namespace Barotrauma return; } + bool relativeStrength = false; + if (args.Length > 4) + { + bool.TryParse(args[4], out relativeStrength); + } + Character targetCharacter = (args.Length <= 2) ? client.Character : FindMatchingCharacter(args.Skip(2).ToArray()); if (targetCharacter != null) { - targetCharacter.CharacterHealth.ApplyAffliction(targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); + Limb targetLimb = targetCharacter.AnimController.MainLimb; + if (args.Length > 3) + { + targetLimb = targetCharacter.AnimController.Limbs.FirstOrDefault(l => l.type.ToString().Equals(args[3], StringComparison.OrdinalIgnoreCase)); + } + if (relativeStrength) + { + afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength; + } + targetCharacter.CharacterHealth.ApplyAffliction(targetLimb ?? targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); } } ); @@ -1732,16 +1748,86 @@ namespace Barotrauma { foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.Character != revivedCharacter) continue; + if (c.Character != revivedCharacter) { continue; } - //clients stop controlling the character when it dies, force control back - GameMain.Server.SetClientCharacter(c, revivedCharacter); + //clients stop controlling the character when it dies, force control back + GameMain.Server.SetClientCharacter(c, revivedCharacter); break; } } } ); + AssignOnClientRequestExecute( + "givetalent", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (args.Length == 0) { return; } + Character targetCharacter = (args.Length >= 2) ? FindMatchingCharacter(args.Skip(1).ToArray(), false) : client.Character; + + if (targetCharacter == null) { return; } + + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => + c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + GameMain.Server.SendConsoleMessage("Couldn't find the talent \"" + args[0] + "\".", client); + return; + } + targetCharacter.GiveTalent(talentPrefab); + NewMessage($"Talent \"{talentPrefab.DisplayName}\" given to \"{targetCharacter.Name}\" by \"{client.Name}\"."); + GameMain.Server.SendConsoleMessage($"Gave talent \"{talentPrefab.DisplayName}\" to \"{targetCharacter.Name}\".", client); + } + ); + + AssignOnClientRequestExecute( + "unlocktalents", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + var targetCharacter = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled; + if (targetCharacter == null) { return; } + + List talentTrees = new List(); + if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) + { + talentTrees.AddRange(TalentTree.JobTalentTrees.Values); + } + else + { + var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (job == null) + { + GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client); + return; + } + if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + { + GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client); + return; + } + talentTrees.Add(talentTree); + } + + foreach (var talentTree in talentTrees) + { + foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var option in subTree.TalentOptionStages) + { + foreach (var talent in option.Talents) + { + targetCharacter.GiveTalent(talent); + NewMessage($"Talent \"{talent.DisplayName}\" given to \"{targetCharacter.Name}\" by \"{client.Name}\"."); + GameMain.Server.SendConsoleMessage($"Gave talent \"{talent.DisplayName}\" to \"{targetCharacter.Name}\".", client); + NewMessage($"Unlocked talent \"{talent.DisplayName}\"."); + } + } + } + } + } + ); + AssignOnClientRequestExecute( "freeze", (Client client, Vector2 cursorWorldPos, string[] args) => @@ -1792,7 +1878,7 @@ namespace Barotrauma "control", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (args.Length < 1) return; + if (args.Length < 1) { return; } var character = FindMatchingCharacter(args, ignoreRemotePlayers: true, allowedRemotePlayer: client); if (character != null) { @@ -2116,6 +2202,7 @@ namespace Barotrauma if (client == null) { GameMain.Server.SendConsoleMessage("Client \"" + args[0] + "\" not found.", senderClient); + return; } var character = FindMatchingCharacter(args.Skip(1).ToArray(), false); @@ -2206,13 +2293,13 @@ namespace Barotrauma { foreach (Skill skill in character.Info.Job.Skills) { - character.Info.SetSkillLevel(skill.Identifier, level, character.WorldPosition); + character.Info.SetSkillLevel(skill.Identifier, level); } GameMain.Server.SendConsoleMessage($"Set all {character.Name}'s skills to {level}", senderClient); } else { - character.Info.SetSkillLevel(skillIdentifier, level, character.WorldPosition); + character.Info.SetSkillLevel(skillIdentifier, level); GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index e649ec598..99e79fc47 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -11,6 +11,7 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); msg.Write((ushort)spawnedItems.Count); foreach (Item item in spawnedItems) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..dd8138d18 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,22 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + msg.Write((ushort)existingTargets.Count); + foreach (var t in existingTargets) + { + msg.Write(t != null ? t.ID : Entity.NullEntityID); + } + msg.Write((ushort)spawnedTargets.Count); + foreach (var t in spawnedTargets) + { + t.WriteSpawnData(msg, t.ID, false); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs deleted file mode 100644 index 0f0a29d29..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class BeaconMission : Mission - { - public override void ServerWriteInitial(IWriteMessage msg, Client c) - { - return; - } - } -} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index e230ab075..10f5ce5ad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -6,6 +6,7 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); msg.Write((ushort)items.Count); foreach (Item item in items) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index c7912eb2a..76e83ca8b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -83,10 +83,5 @@ namespace Barotrauma } } } - - public override void ServerWriteInitial(IWriteMessage msg, Client c) - { - //do nothing - } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs index 5cb7f95bb..060369f9a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -9,6 +9,8 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); + if (characters.Count == 0) { throw new InvalidOperationException("Server attempted to write escort mission data when no characters had been spawned."); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index b6555cb25..b3dd48bff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -6,16 +6,17 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); msg.Write((byte)caves.Count); foreach (var cave in caves) { msg.Write((byte)(Level.Loaded == null || !Level.Loaded.Caves.Contains(cave) ? 255 : Level.Loaded.Caves.IndexOf(cave))); } - foreach (var kvp in SpawnedResources) + foreach (var kvp in spawnedResources) { msg.Write((byte)kvp.Value.Count); - var rotation = ResourceClusters[kvp.Key].Second; + var rotation = resourceClusters[kvp.Key].rotation; msg.Write(rotation); foreach (var r in kvp.Value) { @@ -23,7 +24,7 @@ namespace Barotrauma } } - foreach (var kvp in RelevantLevelResources) + foreach (var kvp in relevantLevelResources) { msg.Write(kvp.Key); msg.Write((byte)kvp.Value.Length); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index dbbc96902..fc1b5041c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -16,6 +16,14 @@ namespace Barotrauma GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); } - public abstract void ServerWriteInitial(IWriteMessage msg, Client c); + public virtual void ServerWriteInitial(IWriteMessage msg, Client c) + { + msg.Write((ushort)State); + } + + public virtual void ServerWrite(IWriteMessage msg) + { + msg.Write((ushort)State); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs index e46a6bf95..937e23511 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs @@ -7,6 +7,8 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); + if (monsters.Count == 0 && monsterPrefabs.Count > 0) { throw new InvalidOperationException("Server attempted to write monster mission data when no monsters had been spawned."); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index d1b702aa1..7d5c88bb1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -8,6 +8,7 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); msg.Write((byte)(selectedCave == null || Level.Loaded == null || !Level.Loaded.Caves.Contains(selectedCave) ? 255 : Level.Loaded.Caves.IndexOf(selectedCave))); msg.Write(nestPosition.X); msg.Write(nestPosition.Y); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs index 52b5687be..4eb529c2e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -9,6 +9,8 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); + // duplicate code from escortmission, should possibly be combined, though additional loot items might be added so maybe not if (characters.Count == 0) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 1ba55eee5..292d53ce0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -15,6 +15,8 @@ namespace Barotrauma public override void ServerWriteInitial(IWriteMessage msg, Client c) { + base.ServerWriteInitial(msg, c); + msg.Write(usedExistingItem); if (usedExistingItem) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..dc5dbff31 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -0,0 +1,37 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + msg.Write((ushort)startingItems.Count); + foreach (var item in startingItems) + { + item.WriteSpawnData(msg, + item.ID, + parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + } + ServerWriteScanTargetStatus(msg); + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + ServerWriteScanTargetStatus(msg); + } + + private void ServerWriteScanTargetStatus(IWriteMessage msg) + { + msg.Write((byte)scanTargets.Count); + foreach (var kvp in scanTargets) + { + msg.Write(kvp.Key != null ? kvp.Key.ID : Entity.NullEntityID); + msg.Write(kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 9df9222e6..d8ca89639 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -19,7 +19,17 @@ namespace Barotrauma { public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; - public static World World; + + private static World world; + public static World World + { + get + { + if (world == null) { world = new World(new Vector2(0, -9.82f)); } + return world; + } + set { world = value; } + } public static GameSettings Config; public static LuaSetup Lua; @@ -125,6 +135,8 @@ namespace Barotrauma ItemAssemblyPrefab.LoadAll(); LevelObjectPrefab.LoadAll(); BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); + TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); + TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); GameModePreset.Init(); DecalManager = new DecalManager(); @@ -183,6 +195,20 @@ namespace Barotrauma } } + public bool TryStartChildServerRelay() + { + for (int i = 0; i < CommandLineArgs.Length; i++) + { + switch (CommandLineArgs[i].Trim()) + { + case "-pipes": + ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); + return true; + } + } + return false; + } + public void StartServer() { string name = "Server"; @@ -268,7 +294,7 @@ namespace Barotrauma i++; break; case "-pipes": - ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); + //handled in TryStartChildServerRelay i += 2; break; } @@ -327,6 +353,7 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); + TryStartChildServerRelay(); Init(); StartServer(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 94152023e..9f3f034b7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,4 +1,6 @@ using Barotrauma.Networking; +using System.Globalization; +using System.Xml.Linq; namespace Barotrauma { @@ -11,11 +13,65 @@ namespace Barotrauma get { return itemData != null; } } - partial void InitProjSpecific(Client client) + public CharacterCampaignData(Client client, bool giveRespawnPenaltyAffliction = false) { + Name = client.Name; ClientEndPoint = client.Connection.EndPointString; SteamID = client.SteamID; CharacterInfo = client.CharacterInfo; + + healthData = new XElement("health"); + client.Character?.CharacterHealth?.Save(healthData); + if (giveRespawnPenaltyAffliction) + { + var respawnPenaltyAffliction = RespawnManager.GetRespawnPenaltyAffliction(); + healthData.Add(new XElement("Affliction", + new XAttribute("identifier", respawnPenaltyAffliction.Identifier), + new XAttribute("strength", respawnPenaltyAffliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); + } + if (client.Character?.Inventory != null) + { + itemData = new XElement("inventory"); + Character.SaveInventory(client.Character.Inventory, itemData); + } + OrderData = new XElement("orders"); + if (client.CharacterInfo != null) + { + CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); + } + } + + + public CharacterCampaignData(XElement element) + { + Name = element.GetAttributeString("name", "Unnamed"); + ClientEndPoint = element.GetAttributeString("endpoint", null) ?? element.GetAttributeString("ip", ""); + string steamID = element.GetAttributeString("steamid", ""); + if (!string.IsNullOrEmpty(steamID)) + { + ulong.TryParse(steamID, out ulong parsedID); + SteamID = parsedID; + } + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "character": + case "characterinfo": + CharacterInfo = new CharacterInfo(subElement); + break; + case "inventory": + itemData = subElement; + break; + case "health": + healthData = subElement; + break; + case "orders": + OrderData = subElement; + break; + } + } } public bool MatchesClient(Client client) @@ -56,6 +112,6 @@ namespace Barotrauma public void ApplyOrderData(Character character) { CharacterInfo.ApplyOrderData(character, OrderData); - } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 3996fdbeb..7dafc70d0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -27,6 +27,29 @@ namespace Barotrauma public bool GameOver { get; private set; } + class SavedExperiencePoints + { + public readonly ulong SteamID; + public readonly string EndPoint; + public readonly int ExperiencePoints; + + public SavedExperiencePoints(Client client) + { + SteamID = client.SteamID; + EndPoint = client.Connection.EndPointString; + ExperiencePoints = client.Character?.Info?.ExperiencePoints ?? 0; + } + + public SavedExperiencePoints(XElement element) + { + SteamID = element.GetAttributeUInt64("steamid", 0); + EndPoint = element.GetAttributeString("endpoint", string.Empty); + ExperiencePoints = element.GetAttributeInt("points", 0); + } + } + + private readonly List savedExperiencePoints = new List(); + public override bool Paused { get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } @@ -155,6 +178,20 @@ namespace Barotrauma c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign))); } + public void SaveExperiencePoints(Client client) + { + ClearSavedExperiencePoints(client); + savedExperiencePoints.Add(new SavedExperiencePoints(client)); + } + public int GetSavedExperiencePoints(Client client) + { + return savedExperiencePoints.Find(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint))?.ExperiencePoints ?? 0; + } + public void ClearSavedExperiencePoints(Client client) + { + savedExperiencePoints.RemoveAll(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint)); + } + public void LoadPets() { if (petsElement != null) @@ -163,7 +200,7 @@ namespace Barotrauma } } - public void SaveInventories() + public void SavePlayers() { List prevCharacterData = new List(characterData); //client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has a character) @@ -172,7 +209,12 @@ namespace Barotrauma //refresh the character data of clients who are still in the server foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.HasSpawned && c.CharacterInfo != null && c.CharacterInfo.CauseOfDeath != null && c.CharacterInfo.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) + if (c.Character != null && c.Character.Info == null) + { + c.Character = null; + } + + if (c.HasSpawned && c.CharacterInfo != null && c.CharacterInfo.CauseOfDeath != null && c.CharacterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { //the client has opted to spawn this round with Reaper's Tax if (c.WaitForNextRoundRespawn.HasValue && !c.WaitForNextRoundRespawn.Value) @@ -183,14 +225,15 @@ namespace Barotrauma continue; } } - if (c.Character?.Info == null) { continue; } - if (c.Character.IsDead && c.Character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) + var characterInfo = c.Character?.Info ?? c.CharacterInfo; + if (characterInfo == null) { continue; } + if (c.CharacterInfo.CauseOfDeath != null && characterInfo.CauseOfDeath.Type != CauseOfDeathType.Disconnected) { - continue; + RespawnManager.ReduceCharacterSkills(characterInfo); } - c.CharacterInfo = c.Character.Info; + c.CharacterInfo = characterInfo; characterData.RemoveAll(cd => cd.MatchesClient(c)); - characterData.Add(new CharacterCampaignData(c)); + characterData.Add(new CharacterCampaignData(c)); } //refresh the character data of clients who aren't in the server anymore @@ -259,6 +302,16 @@ namespace Barotrauma Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + if (success) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Character?.HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter) ?? false) + { + (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c); + } + } + } GameMain.GameSession.EndRound("", traitorResults, transitionType); @@ -266,7 +319,7 @@ namespace Barotrauma if (success) { - SaveInventories(); + SavePlayers(); yield return CoroutineStatus.Running; @@ -965,6 +1018,15 @@ namespace Barotrauma // save bots CrewManager.SaveMultiplayer(modeElement); + XElement savedExperiencePointsElement = new XElement("SavedExperiencePoints"); + foreach (var savedExperiencePoint in savedExperiencePoints) + { + savedExperiencePointsElement.Add(new XElement("Point", + new XAttribute("steamid", savedExperiencePoint.SteamID), + new XAttribute("endpoint", savedExperiencePoint?.EndPoint ?? string.Empty), + new XAttribute("points", savedExperiencePoint.ExperiencePoints))); + } + // save available submarines XElement availableSubsElement = new XElement("AvailableSubs"); for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..460c5ec46 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,20 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(tainted); + if (tainted) + { + msg.Write(selectedTaintedEffect?.UIntIdentifier ?? 0); + } + else + { + msg.Write(selectedEffect?.UIntIdentifier ?? 0); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 7b29551ae..472cf14b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -16,6 +16,12 @@ namespace Barotrauma.Items.Components set { unsentChanges = value; } } + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + pathFinder = null; + } + public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 3cf56ec69..129d6e622 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -38,6 +38,8 @@ namespace Barotrauma.Items.Components msg.Write(deteriorationTimer); msg.Write(deteriorateAlwaysResetTimer); msg.Write(DeteriorateAlways); + msg.Write(tinkeringDuration); + msg.Write(tinkeringStrength); msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..337b721b0 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs @@ -0,0 +1,14 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent, IServerSerializable + { + private float LastSentScanTimer { get; set; } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(scanTimer); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..056410165 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,21 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable + { + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + { + int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); + if (!item.CanClientAccess(c)) { return; } + if (!SendSignal(signalIndex)) { return; } + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} sent a signal \"{Signals[signalIndex]}\" from {item.Name}", ServerLog.MessageType.ItemInteraction); + item.CreateServerEvent(this, new object[] { signalIndex }); + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + Write(msg, extraData); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index b66f144ff..aa3466eb1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components var panel2 = selectedWire.Connections[1]?.ConnectionPanel; if (panel2 != null && panel2 != this) { panel2.item.CreateServerEvent(panel2); } - CoroutineManager.InvokeAfter(() => + CoroutineManager.Invoke(() => { item.CreateServerEvent(this); if (panel1 != null && panel1 != this) { panel1.item.CreateServerEvent(panel1); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 1382bdc27..517f51201 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -19,13 +19,13 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; - ShowOnDisplay(newOutputValue); + ShowOnDisplay(newOutputValue, addToHistory: true); item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input, bool addToHistory = true) + partial void ShowOnDisplay(string input, bool addToHistory) { if (addToHistory) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index ce354ecaf..c7b6eb206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -282,6 +282,7 @@ namespace Barotrauma msg.Write(body == null ? (byte)0 : (byte)body.BodyType); msg.Write(SpawnedInOutpost); msg.Write(AllowStealing); + msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality); byte teamID = 0; foreach (WifiComponent wifiComponent in GetComponents()) @@ -289,6 +290,14 @@ namespace Barotrauma teamID = (byte)wifiComponent.TeamID; break; } + if (teamID == 0) + { + foreach (IdCard idCard in GetComponents()) + { + teamID = (byte)idCard.TeamID; + break; + } + } msg.Write(teamID); bool tagsChanged = tags.Count != prefab.Tags.Count || !tags.All(t => prefab.Tags.Contains(t)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 6226d0b2c..584342950 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Barotrauma.IO; -using System.IO.Pipes; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; +using System.IO.Pipes; namespace Barotrauma.Networking { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 51f6a23ac..e6b822610 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1190,6 +1190,10 @@ namespace Barotrauma.Networking { c.Character.ServerRead(objHeader, inc, c); } + else + { + DebugConsole.AddWarning($"Received character inputs from a client who's not controlling a character ({c.Name})."); + } break; case ClientNetObject.ENTITY_STATE: entityEventManager.Read(inc, c); @@ -1327,7 +1331,7 @@ namespace Barotrauma.Networking Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); if (mpCampaign != null && Level.IsLoadedOutpost) { - mpCampaign.SaveInventories(); + mpCampaign.SavePlayers(); GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } @@ -2363,8 +2367,16 @@ namespace Barotrauma.Networking characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + spawnedCharacter.LoadTalents(); + characterData.HasSpawned = true; } + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null) + { + spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i]))); + mpCampaign.ClearSavedExperiencePoints(teamClients[i]); + } + spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; spawnedCharacter.OwnerClientName = teamClients[i].Name; } @@ -2375,6 +2387,8 @@ namespace Barotrauma.Networking spawnedCharacter.TeamID = teamID; spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); + // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway + spawnedCharacter.LoadTalents(); } } @@ -2447,6 +2461,7 @@ namespace Barotrauma.Networking GameMain.Lua.hook.Call("roundStart", new object[] { }); + startGameCoroutine = null; yield return CoroutineStatus.Success; } @@ -2471,6 +2486,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.AllowRespawn && missionAllowRespawn); msg.Write(serverSettings.AllowDisguises); msg.Write(serverSettings.AllowRewiring); + msg.Write(serverSettings.AllowFriendlyFire); msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.UseRespawnShuttle); @@ -2638,8 +2654,8 @@ namespace Barotrauma.Networking } } - Submarine.Unload(); entityEventManager.Clear(); + Submarine.Unload(); GameMain.NetLobbyScreen.Select(); Log("Round ended.", ServerLog.MessageType.ServerMessage); @@ -3175,28 +3191,19 @@ namespace Barotrauma.Networking public void SendOrderChatMessage(OrderChatMessage message) { if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; } - //ChatMessageType messageType = ChatMessage.CanUseRadio(message.Sender) ? ChatMessageType.Radio : ChatMessageType.Default; - //check which clients can receive the message and apply distance effects foreach (Client client in ConnectedClients) { - string modifiedMessage = message.Text; - - if (message.Sender != null && - client.Character != null && !client.Character.IsDead) + if (message.Sender != null && client.Character != null && !client.Character.IsDead) { //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); } - - string myReceivedMessage = message.Text; - - if (!string.IsNullOrWhiteSpace(myReceivedMessage)) + if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender)); } } @@ -3492,15 +3499,15 @@ namespace Barotrauma.Networking } catch (Exception e) { - //gender = Gender.Male; - //race = Race.White; - //headSpriteId = 0; DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); } int hairIndex = message.ReadByte(); int beardIndex = message.ReadByte(); int moustacheIndex = message.ReadByte(); int faceAttachmentIndex = message.ReadByte(); + Color skinColor = message.ReadColorR8G8B8(); + Color hairColor = message.ReadColorR8G8B8(); + Color facialHairColor = message.ReadColorR8G8B8(); List> jobPreferences = new List>(); int count = message.ReadByte(); @@ -3517,6 +3524,9 @@ namespace Barotrauma.Networking sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + sender.CharacterInfo.SkinColor = skinColor; + sender.CharacterInfo.HairColor = hairColor; + sender.CharacterInfo.FacialHairColor = facialHairColor; if (jobPreferences.Count > 0) { @@ -3816,7 +3826,7 @@ namespace Barotrauma.Networking return preferredClient; } - public void UpdateMissionState(Mission mission, int state) + public void UpdateMissionState(Mission mission) { foreach (var client in connectedClients) { @@ -3824,7 +3834,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.MISSION); int missionIndex = GameMain.GameSession.GetMissionIndex(mission); msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); - msg.Write((ushort)state); + mission?.ServerWrite(msg); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e4976ad1a..b0f520261 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -518,14 +518,22 @@ namespace Barotrauma AdjustKarma(character, karmaIncrease, "Repaired item"); } - public void OnReactorOverHeating(Character character, float deltaTime) + public void OnReactorOverHeating(Item reactor, Character character, float deltaTime) { - AdjustKarma(character, -ReactorOverheatKarmaDecrease * deltaTime, "Caused reactor to overheat"); + if (reactor?.Submarine == null || character == null) { return; } + if (reactor.Submarine.TeamID == CharacterTeamType.FriendlyNPC || reactor.Submarine.TeamID == character.TeamID) + { + AdjustKarma(character, -ReactorOverheatKarmaDecrease * deltaTime, "Caused reactor to overheat"); + } } - public void OnReactorMeltdown(Character character) + public void OnReactorMeltdown(Item reactor, Character character) { - AdjustKarma(character, -ReactorMeltdownKarmaDecrease, "Caused a reactor meltdown"); + if (reactor?.Submarine == null || character == null) { return; } + if (reactor.Submarine.TeamID == CharacterTeamType.FriendlyNPC || reactor.Submarine.TeamID == character.TeamID) + { + AdjustKarma(character, -ReactorMeltdownKarmaDecrease, "Caused a reactor meltdown"); + } } public void OnExtinguishingFire(Character character, float deltaTime) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index dd851251d..95aa0e83f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -44,11 +44,11 @@ namespace Barotrauma.Networking class ServerEntityEventManager : NetEntityEventManager { - private List events; + private readonly List events; //list of unique events (i.e. !IsDuplicate) created during the round //used for syncing clients who join mid-round - private List uniqueEvents; + private readonly List uniqueEvents; private UInt16 lastSentToAll; private UInt16 lastSentToAnyone; @@ -90,11 +90,11 @@ namespace Barotrauma.Networking } } - private List bufferedEvents; + private readonly List bufferedEvents; private UInt16 ID; - private GameServer server; + private readonly GameServer server; private double lastEventCountHighWarning; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index c3c67d149..fb3785123 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -8,6 +8,11 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { + /// + /// How much skills drop towards the job's default skill levels when respawning midround in the campaign + /// + const float SkillReductionOnCampaignMidroundRespawn = 0.75f; + private DateTime despawnTime; private float shuttleEmptyTimer; @@ -305,6 +310,8 @@ namespace Barotrauma.Networking partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) { + respawnedCharacters.Clear(); + var respawnSub = RespawnShuttle ?? Submarine.MainSub; MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; @@ -321,7 +328,7 @@ namespace Barotrauma.Networking if (matchingData != null && !matchingData.HasSpawned) { c.CharacterInfo = matchingData.CharacterInfo; - } + } //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked @@ -376,8 +383,28 @@ namespace Barotrauma.Networking characterInfos[i].ClearCurrentOrders(); - var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); + bool forceSpawnInMainSub = false; + if (!bot && campaign != null) + { + var matchingData = campaign?.GetClientCharacterData(clients[i]); + if (matchingData != null) + { + if (!matchingData.HasSpawned) + { + forceSpawnInMainSub = true; + } + else + { + ReduceCharacterSkills(characterInfos[i]); + } + } + } + + var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); character.TeamID = CharacterTeamType.Team1; + character.LoadTalents(); + + respawnedCharacters.Add(character); if (bot) { @@ -385,6 +412,12 @@ namespace Barotrauma.Networking } else { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && character.Info != null) + { + character.Info.SetExperience(Math.Max(character.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(clients[i]))); + mpCampaign.ClearSavedExperiencePoints(clients[i]); + } + //tell the respawning client they're no longer a traitor if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null) { @@ -476,6 +509,17 @@ namespace Barotrauma.Networking } } + public static void ReduceCharacterSkills(CharacterInfo characterInfo) + { + if (characterInfo?.Job == null) { return; } + foreach (Skill skill in characterInfo.Job.Skills) + { + var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Prefab == s); + if (skillPrefab == null) { continue; } + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.X, SkillReductionOnCampaignMidroundRespawn); + } + } + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index affe37a85..5da3ddd5f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -163,9 +163,7 @@ namespace Barotrauma.Networking RadiationEnabled = incMsg.ReadBoolean(); int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1; - if (maxMissionCount < CampaignSettings.MinMissionCountLimit) maxMissionCount = CampaignSettings.MaxMissionCountLimit; - if (maxMissionCount > CampaignSettings.MaxMissionCountLimit) maxMissionCount = CampaignSettings.MinMissionCountLimit; - MaxMissionCount = maxMissionCount; + MaxMissionCount = MathHelper.Clamp(maxMissionCount, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); changed |= true; } @@ -267,7 +265,7 @@ namespace Barotrauma.Networking "192-255", "384-591", "1024-1279", - "19968-40959","13312-19903","131072-15043983","15043985-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK + "19968-21327","21329-40959","13312-19903","131072-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK }; string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", defaultAllowedClientNameChars); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index acb320756..889f82de4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -92,7 +92,7 @@ namespace Barotrauma case VoteType.Mode: string modeIdentifier = inc.ReadString(); GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier); - if (!mode.Votable) { break; } + if (mode == null || !mode.Votable) { break; } sender.SetVote(voteType, mode); break; case VoteType.EndRound: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs index 582a853fe..a95f200bc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs @@ -185,7 +185,10 @@ namespace Barotrauma { existingItems.Add(item); } - Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory); + Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory, null, item => + { + item.AddTag("traitormissionitem"); + }); target = null; } else if (allowExisting) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 04fdfcb67..538fd90ef 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.14.9.1 + 0.15.12.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer @@ -38,6 +38,7 @@ TRACE;SERVER;WINDOWS;USE_STEAM x64 ..\bin\$(Configuration)Windows\ + true @@ -54,6 +55,7 @@ ..\bin\$(Configuration)Windows\ full true + true diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 27133e747..e317ddd13 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -23,6 +23,7 @@ + @@ -63,6 +64,12 @@ + + + + + + @@ -73,6 +80,7 @@ + @@ -80,6 +88,8 @@ + + @@ -147,6 +157,13 @@ + + + + + + + @@ -155,6 +172,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -261,4 +305,11 @@ + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 4e3f6b91e..719417830 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -1,7 +1,10 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using FarseerPhysics; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.Items.Components; using System.Linq; namespace Barotrauma @@ -94,14 +97,31 @@ namespace Barotrauma } } - protected bool HasValidPath(bool requireNonDirty = false) => - steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty); + public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) => + steeringManager is IndoorsSteeringManager pathSteering && + pathSteering.CurrentPath != null && + (!requireUnfinished || !pathSteering.CurrentPath.Finished) && + !pathSteering.CurrentPath.Unreachable && + (!requireNonDirty || !pathSteering.IsPathDirty); + + public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable; + public bool IsCurrentPathFinished => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Finished; + + protected readonly float colliderWidth; + protected readonly float minGapSize; + protected readonly float colliderLength; + protected readonly float avoidLookAheadDistance; public AIController (Character c) { Character = c; hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer); Enabled = true; + var size = Character.AnimController.Collider.GetSize(); + colliderWidth = size.X; + colliderLength = size.Y; + avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); + minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength)); } public virtual void OnAttacked(Character attacker, AttackResult attackResult) { } @@ -326,7 +346,148 @@ namespace Barotrauma unequippedItems.Clear(); } + #region Escape + public abstract bool Escape(float deltaTime); + + public Gap EscapeTarget { get; private set; } + + private readonly float escapeTargetSeekInterval = 2; + private float escapeTimer; + protected bool allGapsSearched; + protected readonly HashSet unreachableGaps = new HashSet(); + protected bool UpdateEscape(float deltaTime, bool canAttackDoors) + { + IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager; + if (allGapsSearched) + { + escapeTimer -= deltaTime; + if (escapeTimer <= 0) + { + allGapsSearched = false; + } + } + if (Character.CurrentHull != null && pathSteering != null) + { + // Seek exit if inside + if (!allGapsSearched) + { + float closestDistance = 0; + foreach (Gap gap in Gap.GapList) + { + if (gap == null || gap.Removed) { continue; } + if (EscapeTarget == gap) { continue; } + if (unreachableGaps.Contains(gap)) { continue; } + if (gap.Submarine != Character.Submarine) { continue; } + if (gap.IsRoomToRoom) { continue; } + float multiplier = 1; + var door = gap.ConnectedDoor; + if (door != null) + { + if (!door.CanBeTraversed) + { + if (!door.HasAccess(Character)) + { + if (!canAttackDoors) { continue; } + // Treat doors that don't have access to like they were farther, because it will take time to break them. + multiplier = 5; + } + } + } + else + { + if (gap.Open < 1) { continue; } + if (gap.Size < minGapSize) { continue; } + } + if (gap.FlowTargetHull == Character.CurrentHull) + { + // If the gap is in the same room, it's close enough. + EscapeTarget = gap; + break; + } + float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier; + if (EscapeTarget == null || distance < closestDistance) + { + EscapeTarget = gap; + closestDistance = distance; + } + } + allGapsSearched = true; + escapeTimer = escapeTargetSeekInterval; + } + else if (EscapeTarget != null && EscapeTarget.FlowTargetHull != Character.CurrentHull) + { + if (IsCurrentPathUnreachable) + { + unreachableGaps.Add(EscapeTarget); + EscapeTarget = null; + allGapsSearched = false; + } + } + } + if (EscapeTarget != null) + { + var door = EscapeTarget.ConnectedDoor; + bool isClosedDoor = door != null && !door.IsOpen; + Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; + float sqrDist = diff.LengthSquared(); + bool isClose = sqrDist < MathUtils.Pow2(100); + if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished) + { + // Very close to the target, outside, or at the end of the path -> try to steer through the gap + SteeringManager.Reset(); + pathSteering?.ResetPath(); + Vector2 dir = Vector2.Normalize(diff); + if (Character.CurrentHull == null || isClose) + { + // Outside -> steer away from the target + if (EscapeTarget.FlowTargetHull != null) + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(EscapeTarget.WorldPosition - EscapeTarget.FlowTargetHull.WorldPosition)); + } + else + { + SteeringManager.SteeringManual(deltaTime, -dir); + } + } + else + { + // Still inside -> steer towards the target + SteeringManager.SteeringManual(deltaTime, dir); + } + return sqrDist < MathUtils.Pow2(250); + } + else if (pathSteering != null) + { + pathSteering.SteeringSeek(EscapeTarget.SimPosition, weight: 1, minGapSize); + } + else + { + SteeringManager.SteeringSeek(EscapeTarget.SimPosition, 10); + } + } + else + { + // Can't find the target + EscapeTarget = null; + allGapsSearched = false; + unreachableGaps.Clear(); + } + return false; + } + + public void ResetEscape() + { + EscapeTarget = null; + allGapsSearched = false; + unreachableGaps.Clear(); + } + + #endregion + protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } + + public virtual void ClientRead(IReadMessage msg) { } + public virtual void ServerWrite(IWriteMessage msg) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 7479ae3da..b105f540e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -92,7 +92,16 @@ namespace Barotrauma public string SonarLabel; public string SonarIconIdentifier; - public bool Enabled => SoundRange > 0 || SightRange > 0; + private bool inDetectable; + + /// + /// Should be reset to false each frame and kept indetectable by e.g. a status effect. + /// + public bool InDetectable + { + get => inDetectable || (SoundRange <= 0 && SightRange <= 0); + set => inDetectable = value; + } public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; @@ -181,14 +190,15 @@ namespace Barotrauma public void Update(float deltaTime) { - if (Enabled && !Static && FadeOutTime > 0) + InDetectable = false; + if (!Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active - if (!StaticSight) + if (!StaticSight && SightRange > 0) { DecreaseSightRange(deltaTime); } - if (!StaticSound) + if (!StaticSound && SoundRange > 0) { DecreaseSoundRange(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index ba65f0c2c..440bf76ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -10,7 +11,7 @@ using System.Linq; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow, FleeTo, Patrol } public enum AttackPattern { Straight, Sweep, Circle } @@ -37,6 +38,12 @@ namespace Barotrauma PreviousState = _state; OnStateChanged(_state, value); _state = value; + if (_state == AIState.Attack) + { +#if CLIENT + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); +#endif + } } } @@ -50,8 +57,8 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; private readonly float attackLimbResetInterval = 2; - - private readonly float avoidLookAheadDistance; + // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. + private const float minPriority = 10; private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; private SteeringManager outsideSteering, insideSteering; @@ -82,6 +89,10 @@ namespace Barotrauma { _previousAttackingLimb = _attackingLimb; } + if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) + { + SetAimTimer(); + } _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -104,20 +115,21 @@ namespace Barotrauma lastAttackUpdateTime = Timing.TotalTime; } } - + + public AITargetMemory SelectedTargetMemory => selectedTargetMemory; private AITargetMemory selectedTargetMemory; private float targetValue; private CharacterParams.TargetParams selectedTargetingParams; private Dictionary targetMemories; - private readonly float colliderWidth; - private readonly float colliderLength; private readonly int requiredHoleCount; private bool canAttackWalls; + public bool CanAttackDoors => canAttackDoors; private bool canAttackDoors; private bool canAttackCharacters; + public float PriorityFearIncrement => priorityFearIncreasement; private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; @@ -136,6 +148,8 @@ namespace Barotrauma private CirclePhase CirclePhase; private float currentAttackIntensity; + private CoroutineHandle disableTailCoroutine; + private readonly IEnumerable myBodies; public LatchOntoAI LatchOntoAI { get; private set; } @@ -167,7 +181,7 @@ namespace Barotrauma get { //can't enter a submarine when attached to something - return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttached); + return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub); } } @@ -178,7 +192,7 @@ namespace Barotrauma //can't flip when attached to something, when eating, or reversing or in a (relatively) small room return !Reverse && (State != AIState.Eat || Character.SelectedCharacter == null) && - (LatchOntoAI == null || !LatchOntoAI.IsAttached) && + (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub) && (Character.CurrentHull == null || !Character.AnimController.InWater || Math.Min(Character.CurrentHull.Size.X, Character.CurrentHull.Size.Y) > ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth))); } } @@ -286,16 +300,12 @@ namespace Barotrauma ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, Character.IsHumanoid, canAttackDoors); + insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; - var size = Character.AnimController.Collider.GetSize(); - colliderWidth = size.X; - colliderLength = size.Y; requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); } @@ -394,18 +404,27 @@ namespace Barotrauma public void SelectTarget(AITarget target, float priority) { SelectedAiTarget = target; - selectedTargetMemory = GetTargetMemory(target, true); + selectedTargetMemory = GetTargetMemory(target, addIfNotFound: true); selectedTargetMemory.Priority = priority; ignoredTargets.Remove(target); } private float movementMargin; + private void ReleaseDragTargets() + { + if (Character.Inventory != null) + { + Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); + } + } + public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } base.Update(deltaTime); UpdateTriggers(deltaTime); + Character.ClearInputs(); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) @@ -431,10 +450,10 @@ namespace Barotrauma if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) { - if (SelectedAiTarget?.Entity != null || escapeTarget != null) + if (SelectedAiTarget?.Entity != null || EscapeTarget != null) { - Entity t = SelectedAiTarget?.Entity ?? escapeTarget; - float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; + Entity t = SelectedAiTarget?.Entity ?? EscapeTarget; + float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; } else @@ -442,10 +461,9 @@ namespace Barotrauma Character.AnimController.TargetDir = Character.AnimController.movement.X > 0.0f ? Direction.Right : Direction.Left; } } - if (isStateChanged) { - if (State == AIState.Idle) + if (State == AIState.Idle || State == AIState.Patrol) { stateResetTimer -= deltaTime; if (stateResetTimer <= 0) @@ -505,7 +523,9 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } - if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive) + if (SelectedAiTarget?.Entity != null && + (LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && + (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { UpdateWallTarget(requiredHoleCount); } @@ -538,7 +558,7 @@ namespace Barotrauma } else { - if (Character.Submarine != null) + if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) { @@ -566,6 +586,9 @@ namespace Barotrauma case AIState.Idle: UpdateIdle(deltaTime); break; + case AIState.Patrol: + UpdatePatrol(deltaTime); + break; case AIState.Attack: run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); @@ -576,7 +599,7 @@ namespace Barotrauma case AIState.Escape: case AIState.Flee: run = true; - UpdateEscape(deltaTime); + Escape(deltaTime); break; case AIState.Avoid: case AIState.PassiveAggressive: @@ -593,7 +616,7 @@ namespace Barotrauma run = true; if (State == AIState.Avoid) { - UpdateEscape(deltaTime); + Escape(deltaTime); } else { @@ -628,6 +651,7 @@ namespace Barotrauma break; case AIState.Protect: case AIState.Follow: + case AIState.FleeTo: if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; @@ -641,9 +665,9 @@ namespace Barotrauma { Character c = a.Character; if (c.IsDead || c.Removed) { return false; } - if (!IsFriendly(Character, c)) { return true; } + if (!Character.IsFriendly(c)) { return true; } // Only apply the threshold to friendly characters - return a.Damage >= selectedTargetingParams.DamageThreshold; + return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; if (attacker != null) @@ -656,17 +680,75 @@ namespace Barotrauma } } float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); - if (sqrDist > Math.Pow(reactDist + movementMargin, 2)) + float reactDist = GetPerceivingRange(SelectedAiTarget); + Vector2 offset = Vector2.Zero; + if (selectedTargetingParams != null) { - movementMargin = reactDist; + if (selectedTargetingParams.ReactDistance > 0) + { + reactDist = selectedTargetingParams.ReactDistance; + } + offset = selectedTargetingParams.Offset; + } + if (offset != Vector2.Zero) + { + reactDist += offset.Length(); + } + if (sqrDist > MathUtils.Pow2(reactDist + movementMargin)) + { + movementMargin = State == AIState.FleeTo ? 0 : reactDist; run = true; UpdateFollow(deltaTime); } else { movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist); - UpdateIdle(deltaTime); + if (State == AIState.FleeTo) + { + SteeringManager.Reset(); + Character.AnimController.TargetMovement = Vector2.Zero; + if (Character.AnimController.InWater) + { + float force = Character.AnimController.Collider.Mass / 10; + Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition + ConvertUnits.ToSimUnits(offset), force); + if (SelectedAiTarget.Entity is Item item) + { + float rotation = item.Rotation; + Character.AnimController.Collider.SmoothRotate(rotation, Character.AnimController.SwimFastParams.SteerTorque); + var mainLimb = Character.AnimController.MainLimb; + if (mainLimb.type == LimbType.Head) + { + mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.HeadTorque); + } + else + { + mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque); + } + } + if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag("guardianshelter")) + { + if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine)) + { + disableTailCoroutine = CoroutineManager.Invoke(() => + { + if (Character != null && !Character.Removed) + { + Character.AnimController.HideAndDisable(LimbType.Tail, ignoreCollisions: false); + } + }, 1f); + } + } + Character.AnimController.ApplyPose( + new Vector2(0, -1), + new Vector2(0, -1), + new Vector2(0, -1), + new Vector2(0, -1), footMoveForce: 1); + } + } + else + { + UpdateIdle(deltaTime); + } } break; case AIState.Observe: @@ -759,6 +841,10 @@ namespace Barotrauma private void UpdateIdle(float deltaTime, bool followLastTarget = true) { + if (AIParams.PatrolFlooded || AIParams.PatrolDry) + { + State = AIState.Patrol; + } var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { @@ -816,138 +902,129 @@ namespace Barotrauma } } - #endregion + private readonly List targetHulls = new List(); + private readonly List hullWeights = new List(); - #region Escape - private readonly float escapeTargetSeekInterval = 2; - private float escapeTimer; - private Gap escapeTarget; - private bool allGapsSearched; - private readonly HashSet unreachableGaps = new HashSet(); - private void UpdateEscape(float deltaTime) + private Hull patrolTarget; + private float newPatrolTargetTimer; + private float patrolTimerMargin; + private readonly float newPatrolTargetIntervalMin = 5; + private readonly float newPatrolTargetIntervalMax = 30; + private bool searchingNewHull; + + private void UpdatePatrol(float deltaTime, bool followLastTarget = true) { - if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)) + if (SteeringManager is IndoorsSteeringManager pathSteering) { - State = AIState.Idle; - return; - } - else if (selectedTargetMemory != null && SelectedAiTarget?.Entity is Character) - { - selectedTargetMemory.Priority += deltaTime * priorityFearIncreasement; - } - IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager; - bool hasValidPath = pathSteering?.CurrentPath != null && !pathSteering.IsPathDirty && !pathSteering.CurrentPath.Unreachable; - if (allGapsSearched) - { - escapeTimer -= deltaTime; - if (escapeTimer <= 0) + if (patrolTarget == null || IsCurrentPathUnreachable || IsCurrentPathFinished) { - allGapsSearched = false; + newPatrolTargetTimer = Math.Min(newPatrolTargetTimer, newPatrolTargetIntervalMin); } - } - if (Character.CurrentHull != null && pathSteering != null) - { - // Seek exit if inside - if (!allGapsSearched) + if (newPatrolTargetTimer > 0) { - float closestDistance = 0; - foreach (Gap gap in Gap.GapList) + newPatrolTargetTimer -= deltaTime; + } + else + { + if (!searchingNewHull) { - if (gap == null || gap.Removed) { continue; } - if (escapeTarget == gap) { continue; } - if (unreachableGaps.Contains(gap)) { continue; } - if (gap.Submarine != Character.Submarine) { continue; } - if (gap.IsRoomToRoom) { continue; } - float multiplier = 1; - var door = gap.ConnectedDoor; - if (door != null) + searchingNewHull = true; + FindTargetHulls(); + } + else if (targetHulls.Any()) + { + patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); + var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, Character.Submarine, minGapSize: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n)); + if (path.Unreachable) { - if (!door.CanBeTraversed) - { - if (!door.HasAccess(Character)) - { - if (!canAttackDoors) { continue; } - // Treat doors that don't have access to like they were farther, because it will take time to break them. - multiplier = 5; - } - } + //can't go to this room, remove it from the list and try another room + int index = targetHulls.IndexOf(patrolTarget); + targetHulls.RemoveAt(index); + hullWeights.RemoveAt(index); + PathSteering.Reset(); + patrolTarget = null; + patrolTimerMargin += 0.5f; + patrolTimerMargin = Math.Min(patrolTimerMargin, newPatrolTargetIntervalMin); + newPatrolTargetTimer = Math.Min(newPatrolTargetIntervalMin, patrolTimerMargin); } else { - if (gap.Open < 1) { continue; } - bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size; - if (!canGetThrough) { continue; } - } - if (gap.FlowTargetHull == Character.CurrentHull) - { - // If the gap is in the same room, it's close enough. - escapeTarget = gap; - break; - } - float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier; - if (escapeTarget == null || distance < closestDistance) - { - escapeTarget = gap; - closestDistance = distance; + PathSteering.SetPath(path); + patrolTimerMargin = 0; + newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f); + searchingNewHull = false; } } - allGapsSearched = true; - escapeTimer = escapeTargetSeekInterval; - } - else if (escapeTarget != null && escapeTarget.FlowTargetHull != Character.CurrentHull) - { - if (pathSteering.CurrentPath != null && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + else { - unreachableGaps.Add(escapeTarget); - escapeTarget = null; - allGapsSearched = false; + // Couldn't find a valid hull + newPatrolTargetTimer = newPatrolTargetIntervalMax; + searchingNewHull = false; } } - } - if (escapeTarget != null && Character.CurrentHull != null && Vector2.DistanceSquared(Character.SimPosition, escapeTarget.SimPosition) > 0.5f) - { - if (hasValidPath && pathSteering.CurrentPath.Finished) + if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable) { - // Steer manually towards the gap - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(escapeTarget.WorldPosition - Character.WorldPosition)); - } - else if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull) - { - // Steer away from the target if in the same room - Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); - if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; } - SteeringManager.SteeringManual(deltaTime, escapeDir); + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n)); return; } - else if (pathSteering != null) + } + + bool PatrolNodeFilter(PathNode n) => + AIParams.PatrolFlooded && (Character.CurrentHull == null || n.Waypoint.CurrentHull == null || n.Waypoint.CurrentHull.WaterPercentage >= 80) || + AIParams.PatrolDry && Character.CurrentHull != null && n.Waypoint.CurrentHull != null && n.Waypoint.CurrentHull.WaterPercentage <= 50; + + UpdateIdle(deltaTime, followLastTarget); + } + + private void FindTargetHulls() + { + if (Character.Submarine == null) { return; } + if (Character.CurrentHull == null) { return; } + targetHulls.Clear(); + hullWeights.Clear(); + float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2); + bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry; + foreach (var hull in Hull.hullList) + { + if (hull.Submarine == null) { continue; } + if (hull.Submarine.TeamID != Character.Submarine.TeamID) { continue; } + if (!Character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } + if (hull.RectWidth < hullMinSize || hull.RectHeight < hullMinSize) { continue; } + if (checkWaterLevel) { - if (hasValidPath && canAttackDoors) + if (AIParams.PatrolDry) { - var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) - { - if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack) - { - SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); - State = AIState.Attack; - return; - } - } + if (hull.WaterPercentage > 50) { continue; } + } + if (AIParams.PatrolFlooded) + { + if (hull.WaterPercentage < 80) { continue; } } } - SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10); - } - else - { - escapeTarget = null; - allGapsSearched = false; - Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); - if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; - SteeringManager.SteeringManual(deltaTime, escapeDir); - if (Character.CurrentHull == null) + if (AIParams.PatrolDry && hull.WaterPercentage < 80) { - SteeringManager.SteeringWander(); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + if (Math.Abs(Character.CurrentHull.WorldPosition.Y - hull.WorldPosition.Y) > Character.CurrentHull.CeilingHeight / 2) + { + // Ignore dry hulls that are on a different level + continue; + } + } + if (!targetHulls.Contains(hull)) + { + targetHulls.Add(hull); + float weight = hull.Size.Combine(); + float dist = Vector2.Distance(Character.WorldPosition, hull.WorldPosition); + float optimal = 1000; + float max = 3000; + // Prefer rooms that are far but not too far. + float distanceFactor = dist > optimal ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(optimal, max, dist)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, optimal, dist)); + float waterFactor = 1; + if (checkWaterLevel) + { + waterFactor = AIParams.PatrolDry ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)); + } + weight *= distanceFactor * waterFactor; + hullWeights.Add(weight); } } } @@ -976,7 +1053,7 @@ namespace Barotrauma Character owner = GetOwner(item); if (owner != null) { - if (IsFriendly(Character, owner)) + if (Character.IsFriendly(owner)) { ResetAITarget(); State = AIState.Idle; @@ -1210,7 +1287,7 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb == null || _previousAiTarget != SelectedAiTarget) + if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) { AttackingLimb = GetAttackLimb(attackWorldPos); } @@ -1337,7 +1414,7 @@ namespace Barotrauma } else { - canAttack = Character.CharacterList.All(c => c == Character || !IsFriendly(Character, c) || IsFarEnough(c)); + canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c)); } if (canAttack) { @@ -1356,7 +1433,7 @@ namespace Barotrauma { hitTarget = limb.character; } - if (hitTarget != null && !hitTarget.IsDead && IsFriendly(Character, hitTarget)) + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) { return true; } @@ -1394,6 +1471,8 @@ namespace Barotrauma State = AIState.Idle; return; } + + var pathSteering = SteeringManager as IndoorsSteeringManager; if (AttackingLimb != null && AttackingLimb.attack.Retreat) { @@ -1408,7 +1487,7 @@ namespace Barotrauma Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; steerPos += offset; } - if (SteeringManager is IndoorsSteeringManager pathSteering) + if (pathSteering != null) { if (pathSteering.CurrentPath != null) { @@ -1430,244 +1509,298 @@ namespace Barotrauma } } } - // Steer towards the target if in the same room and swimming - if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && - (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)))) + // When pursuing, we don't want to pursue too close + float max = 300; + float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; + if (!canAttack || distance > margin) { - Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); + // Steer towards the target if in the same room and swimming + // Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering. + if (Character.CurrentHull != null && + Character.Submarine != null && !Character.Submarine.Info.IsRuin && + (Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + { + Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); + } + else + { + pathSteering.SteeringSeek(steerPos, weight: 2, + minGapWidth: minGapSize, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), + checkVisiblity: true); + + if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + { + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + return; + } + } } else { - pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true); - if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + if (AttackingLimb.attack.Ranged) { - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - return; + float dir = Character.AnimController.Dir; + if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) + { + SteeringManager.Reset(); + } + else + { + // Too close + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + } + else + { + // Close enough + SteeringManager.Reset(); } } } else { - SteeringManager.SteeringSeek(steerPos, 5); + pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize); } } else { - switch (selectedTargetingParams.AttackPattern) + // Sweeping and circling doesn't work well inside + if (Character.CurrentHull == null) { - case AttackPattern.Sweep: - if (selectedTargetingParams.SweepDistance > 0) - { - if (distance <= 0) + switch (selectedTargetingParams.AttackPattern) + { + case AttackPattern.Sweep: + if (selectedTargetingParams.SweepDistance > 0) { - distance = (attackWorldPos - WorldPosition).Length(); - } - float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); - if (amplitude > 0) - { - sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; - float sin = (float)Math.Sin(sweepTimer) * amplitude; - steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); - } - else - { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; - } - } - break; - case AttackPattern.Circle: - if (IsCoolDownRunning) { break; } - if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } - if (selectedTargetingParams == null) { break; } - var targetSub = SelectedAiTarget.Entity?.Submarine; - if (targetSub == null) { break; } - float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; - float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); - switch (CirclePhase) - { - case CirclePhase.Start: - currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); - inverseDir = false; - circleDir = GetDirFromHeadingInRadius(); - circleRotation = 0; - strikeTimer = 0; - blockCheckTimer = 0; - breakCircling = false; - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; - float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; - float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; - // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. - // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. - circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); - canAttack = false; - aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); - if (targetSub.Borders.Width < 1000) + if (distance <= 0) { - breakCircling = true; - CirclePhase = CirclePhase.CloseIn; + distance = (attackWorldPos - WorldPosition).Length(); } - else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); + if (amplitude > 0) { - CirclePhase = CirclePhase.CloseIn; - } - else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.FallBack; + sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; + float sin = (float)Math.Sin(sweepTimer) * amplitude; + steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); } else { - CirclePhase = CirclePhase.Advance; + sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; } - break; - case CirclePhase.CloseIn: - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) - { - strikeTimer = AttackingLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; - } - else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) - { - CirclePhase = CirclePhase.Advance; - } - canAttack = false; - break; - case CirclePhase.FallBack: - bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); - if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.Advance; + } + break; + case AttackPattern.Circle: + if (IsCoolDownRunning) { break; } + if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } + if (selectedTargetingParams == null) { break; } + var targetSub = SelectedAiTarget.Entity?.Submarine; + if (targetSub == null) { break; } + float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; + float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + switch (CirclePhase) + { + case CirclePhase.Start: + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + inverseDir = false; + circleDir = GetDirFromHeadingInRadius(); + circleRotation = 0; + strikeTimer = 0; + blockCheckTimer = 0; + breakCircling = false; + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; + float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. + // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); + canAttack = false; + aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); + if (targetSub.Borders.Width < 1000) + { + breakCircling = true; + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + { + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.FallBack; + } + else + { + CirclePhase = CirclePhase.Advance; + } break; - } - return; - case CirclePhase.Advance: - Vector2 subSpeed = targetSub.Velocity; - float requiredDistMultiplier = 1; - // If the target sub is moving fast, just steer towards the target until close enough to strike - if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) - { - CirclePhase = CirclePhase.CloseIn; - } - else - { - circleRotation += deltaTime * circleRotationSpeed * circleDir; - if (circleRotation < -360) + case CirclePhase.CloseIn: + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - circleRotation += 360; + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; } - else if (circleRotation > 360) + else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) { - circleRotation -= 360; + CirclePhase = CirclePhase.Advance; } - Vector2 targetPos = attackSimPos + circleOffset; - if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + canAttack = false; + break; + case CirclePhase.FallBack: + bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); + if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) { - // Too close to the target point - // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, - // which makes it continue circling around the point (as supposed) - // But when there is some offset and the offset is too near, this is not what we want. - if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.Strike; - strikeTimer = AttackingLimb.attack.CoolDown; - } - else - { - CirclePhase = CirclePhase.Start; - } + CirclePhase = CirclePhase.Advance; break; } - steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); - if (IsBlocked(deltaTime, steerPos)) + return; + case CirclePhase.Advance: + Vector2 subSpeed = targetSub.Velocity; + float requiredDistMultiplier = 1; + // If the target sub is moving fast, just steer towards the target until close enough to strike + if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) { - if (!inverseDir) + CirclePhase = CirclePhase.CloseIn; + } + else + { + circleRotation += deltaTime * circleRotationSpeed * circleDir; + if (circleRotation < -360) { - // First try changing the direction - circleDir = -circleDir; - inverseDir = true; + circleRotation += 360; } - else if (circleRotationSpeed < 1) + else if (circleRotation > 360) { - // Then try increasing the rotation speed to change the movement curve - circleRotationSpeed *= 1.1f; + circleRotation -= 360; } - else if (circleOffset.LengthSquared() > 0.1f) + Vector2 targetPos = attackSimPos + circleOffset; + if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) { - // Then try removing the offset - circleOffset = Vector2.Zero; + // Too close to the target point + // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, + // which makes it continue circling around the point (as supposed) + // But when there is some offset and the offset is too near, this is not what we want. + if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.Strike; + strikeTimer = AttackingLimb.attack.CoolDown; + } + else + { + CirclePhase = CirclePhase.Start; + } + break; } - else + steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); + requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); + if (IsBlocked(deltaTime, steerPos)) { - // If we still fail, just steer towards the target - breakCircling = true; + if (!inverseDir) + { + // First try changing the direction + circleDir = -circleDir; + inverseDir = true; + } + else if (circleRotationSpeed < 1) + { + // Then try increasing the rotation speed to change the movement curve + circleRotationSpeed *= 1.1f; + } + else if (circleOffset.LengthSquared() > 0.1f) + { + // Then try removing the offset + circleOffset = Vector2.Zero; + } + else + { + // If we still fail, just steer towards the target + breakCircling = true; + } } } - } - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) - { - strikeTimer = AttackingLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; - } - canAttack = false; - break; - case CirclePhase.Strike: - strikeTimer -= deltaTime; - // just continue the movement forward to make it possible to evade the attack - steerPos = SimPosition + Steering; - if (strikeTimer <= 0) - { - CirclePhase = CirclePhase.Start; - aggressionIntensity += AIParams.AggressionCumulation; - } - break; - } - break; - - bool IsFacing(float margin) - { - float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; - } - - float GetStrikeDistanceMultiplier(Vector2 subSpeed) - { - float requiredDistMultiplier = 2; - bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; - if (isHeading) - { - requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; - float subSpeedHorizontal = Math.Abs(subSpeed.X); - if (subSpeedHorizontal > 1) - { - // Reduce the required distance if the target is moving. - requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); - if (requiredDistMultiplier < 2) + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - requiredDistMultiplier = 2; + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; } - } + canAttack = false; + break; + case CirclePhase.Strike: + strikeTimer -= deltaTime; + // just continue the movement forward to make it possible to evade the attack + steerPos = SimPosition + Steering; + if (strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + aggressionIntensity += AIParams.AggressionCumulation; + } + break; } - return requiredDistMultiplier; - } + break; - float GetDirFromHeadingInRadius() - { - Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); - float angle = MathUtils.VectorToAngle(heading); - return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; - } + bool IsFacing(float margin) + { + float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; + } - float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + float GetStrikeDistanceMultiplier(Vector2 subSpeed) + { + float requiredDistMultiplier = 2; + bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + if (isHeading) + { + requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; + float subSpeedHorizontal = Math.Abs(subSpeed.X); + if (subSpeedHorizontal > 1) + { + // Reduce the required distance if the target is moving. + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + if (requiredDistMultiplier < 2) + { + requiredDistMultiplier = 2; + } + } + } + return requiredDistMultiplier; + } + + float GetDirFromHeadingInRadius() + { + Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + float angle = MathUtils.VectorToAngle(heading); + return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + } } - SteeringManager.SteeringSeek(steerPos, 10); - if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + + if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else if (AttackingLimb.attack.Ranged) + { + // Too close + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } @@ -1686,12 +1819,39 @@ namespace Barotrauma } } + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) + { + if (attackingLimb == null) { return false; } + if (target == null) { return false; } + var attack = attackingLimb.attack; + if (attack == null) { return false; } + if (attack.CoolDownTimer > 0) { return false; } + if (!attack.IsValidContext(currentContexts)) { return false; } + if (!attack.IsValidTarget(target)) { return false; } + if (target is ISerializableEntity se && target is Character) + { + if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + } + if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; } + if (attack.Ranged) + { + // Check that is approximately facing the target + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - attackLimbPos; + float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float angle = VectorExtensions.Angle(forward, toTarget); + if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } + } + return true; + } + private readonly List attackLimbs = new List(); private readonly List weights = new List(); private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; + IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1699,28 +1859,7 @@ namespace Barotrauma { if (limb == ignoredLimb) { continue; } if (limb.IsSevered || limb.IsStuck) { continue; } - if (limb.Disabled) { continue; } - var attack = limb.attack; - if (attack == null) { continue; } - if (attack.CoolDownTimer > 0) { continue; } - if (!attack.IsValidContext(currentContexts)) { continue; } - if (!attack.IsValidTarget(target as IDamageable)) { continue; } - if (target is ISerializableEntity se && target is Character) - { - if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; } - } - if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } - if (attack.Ranged) - { - // Check that is approximately facing the target - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : limb.WorldPosition; - Vector2 toTarget = attackWorldPos - attackLimbPos; - float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(limb.body.TransformedRotation - offset * Character.AnimController.Dir); - float angle = VectorExtensions.Angle(forward, toTarget); - if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { continue; } - } - + if (!IsValidAttack(limb, currentContexts, target)) { continue; } if (AIParams.RandomAttack) { attackLimbs.Add(limb); @@ -1764,7 +1903,11 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - bool isFriendly = IsFriendly(Character, attacker); + if (Character.Params.CanInteract && attackResult.Damage > 10) + { + ReleaseDragTargets(); + } + bool isFriendly = Character.IsFriendly(attacker); if (wasLatched) { State = AIState.Escape; @@ -1775,7 +1918,6 @@ namespace Barotrauma } return; } - if (State == AIState.Flee) { if (!isFriendly) @@ -1840,7 +1982,7 @@ namespace Barotrauma } } - AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true); + AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown @@ -1878,52 +2020,120 @@ namespace Barotrauma } } + private Item GetEquippedItem(Limb limb) + { + InvSlotType GetInvSlotForLimb() + { + return limb.type switch + { + LimbType.RightHand => InvSlotType.RightHand, + LimbType.LeftHand => InvSlotType.LeftHand, + LimbType.Head => InvSlotType.Head, + _ => InvSlotType.None, + }; + } + var slot = GetInvSlotForLimb(); + if (slot != InvSlotType.None) + { + return Character.Inventory.GetItemInLimbSlot(slot); + } + return null; + } + // 10 dmg, 100 health -> 0.1 private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } - - ActiveAttack = attackingLimb?.attack; - + if (attackingLimb?.attack == null) { return false; } + ActiveAttack = attackingLimb.attack; if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. var aiTarget = wallTarget.Structure.AiTarget; if (aiTarget != null && SelectedAiTarget != aiTarget) { - SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority); + SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); State = AIState.Attack; } } IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; if (damageTarget != null) { + if (Character.Params.CanInteract && Character.Inventory != null) + { + // Use equipped items (weapons) + Item item = GetEquippedItem(attackingLimb); + if (item != null) + { + if (item.RequireAimToUse) + { + if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + { + // Valid target, but can't shoot -> return true so that it will not be ignored. + return true; + } + } + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } + } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); -#if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] + if (!ActiveAttack.IsRunning) { - Networking.NetEntityEvent.Type.SetAttackTarget, - attackingLimb, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, - SimPosition.X, - SimPosition.Y - }); +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] + { + Networking.NetEntityEvent.Type.SetAttackTarget, + attackingLimb, + (damageTarget as Entity)?.ID ?? Entity.NullEntityID, + damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, + SimPosition.X, + SimPosition.Y + }); +#else + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif + } + if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { - if (damageTarget.Health > 0 && attackResult.Damage > 0) + if (attackingLimb.attack.CoolDownTimer > 0) { + SetAimTimer(); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed; + float greed = AIParams.AggressionGreed; + if (!(damageTarget is Character)) + { + // Halve the greed for attacking non-characters. + greed /= 2; + } + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } - else + if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) { - selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); - return selectedTargetMemory.Priority > 1; + LatchOntoAI.SetAttachTarget(targetCharacter); + } + if (!attackingLimb.attack.Ranged) + { + if (damageTarget.Health > 0 && attackResult.Damage > 0) + { + // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon + float greed = AIParams.AggressionGreed; + if (!(damageTarget is Character)) + { + // Halve the greed for attacking non-characters. + greed /= 2; + } + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; + } + else + { + selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); + return selectedTargetMemory.Priority > 1; + } } } return true; @@ -1931,6 +2141,64 @@ namespace Barotrauma return false; } + private float aimTimer; + private float visibilityCheckTimer; + private bool canSeeTarget; + private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) + { + if (target == null || weapon == null) { return false; } + Character.CursorPosition = target.WorldPosition; + if (Character.Submarine != null) + { + Character.CursorPosition -= Character.Submarine.Position; + } + visibilityCheckTimer -= deltaTime; + if (visibilityCheckTimer <= 0.0f) + { + canSeeTarget = Character.CanSeeTarget(target); + visibilityCheckTimer = 0.2f; + } + if (!canSeeTarget) + { + SetAimTimer(); + return false; + } + Character.SetInput(InputType.Aim, false, true); + if (aimTimer > 0) + { + aimTimer -= deltaTime; + return false; + } + Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; + float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget); + float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length())); + float margin = MathHelper.PiOver4 * distanceFactor; + if (angle < margin) + { + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); + if (pickedBody != null) + { + Character t = null; + if (pickedBody.UserData is Character c) + { + t = c; + } + else if (pickedBody.UserData is Limb limb) + { + t = limb.character; + } + if (t != null && (t == target || !Character.IsFriendly(t))) + { + return true; + } + } + } + return false; + } + + private void SetAimTimer(float timer = 1.5f) => aimTimer = timer * Rand.Range(0.75f, 1.25f); + private readonly float blockCheckInterval = 0.1f; private float blockCheckTimer; private bool isBlocked; @@ -2052,41 +2320,40 @@ namespace Barotrauma State = AIState.Idle; return; } - if (Character.CurrentHull != null && PathSteering != null) + if (Character.CurrentHull != null && steeringManager == insideSteering) { - // Inside - Character targetCharacter = SelectedAiTarget.Entity as Character; + // Inside, but not inside ruins if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) && - (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) + Character.Submarine != null && !Character.Submarine.Info.IsRuin && + SelectedAiTarget.Entity is Character c && VisibleHulls.Contains(c.CurrentHull)) { // Steer towards the target if in the same room and swimming - Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition); - if (MathUtils.IsValid(dir)) - { - SteeringManager.SteeringManual(deltaTime, dir); - } + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); } else { // Use path finding - SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 2); - if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable) - { - // Can't reach - State = AIState.Idle; - return; - } + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize); } } else { // Outside SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5); - if (Character.AnimController.InWater) + } + if (steeringManager is IndoorsSteeringManager pathSteering) + { + if (!pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable) { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + // Can't reach + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); } } + else if (Character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } } #region Targeting @@ -2105,7 +2372,7 @@ namespace Barotrauma foreach (AITarget aiTarget in AITarget.List) { - if (!aiTarget.Enabled) { continue; } + if (aiTarget.InDetectable) { continue; } if (aiTarget.Entity == null) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y) @@ -2125,6 +2392,9 @@ namespace Barotrauma string targetingTag = null; if (targetCharacter != null) { + // ignore if target is tagged to be explicitly ignored (Feign Death) + if (targetCharacter.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { continue; } + if (targetCharacter.IsDead) { targetingTag = "dead"; @@ -2139,7 +2409,7 @@ namespace Barotrauma } else { - if (IsFriendly(Character, targetCharacter)) + if (Character.IsFriendly(targetCharacter)) { continue; } @@ -2186,10 +2456,12 @@ namespace Barotrauma } else { - // Ignore all structures, items, and hulls inside wrecks and beacons + // Ignore all structures, items, and hulls inside these subs. if (aiTarget.Entity.Submarine != null) { - if (aiTarget.Entity.Submarine.Info.IsWreck || aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) + if (aiTarget.Entity.Submarine.Info.IsWreck || + aiTarget.Entity.Submarine.Info.IsBeacon || + UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } @@ -2200,6 +2472,7 @@ namespace Barotrauma if (character.CurrentHull != null) { continue; } // Ignore ruins if (hull.Submarine == null) { continue; } + if (hull.Submarine.Info.IsRuin) { continue; } } Door door = null; @@ -2215,14 +2488,6 @@ namespace Barotrauma continue; } } - if (door == null) - { - // Ignore items inside ruins, unless we are in the same hull. We can't target the ruin walls. - if (item.Submarine == null && item.CurrentHull != Character.CurrentHull) - { - continue; - } - } foreach (var prio in AIParams.Targets) { if (item.HasTag(prio.Tag)) @@ -2240,6 +2505,11 @@ namespace Barotrauma else if (targetingFromOutsideToInside) { targetingTag = "room"; + if (item.Submarine?.Info.IsRuin != null) + { + // Ignore ruin items when the creature is outside. + continue; + } } } else if (targetingTag == "nasonov") @@ -2266,6 +2536,7 @@ namespace Barotrauma } if (s.IsPlatform) { continue; } if (s.Submarine == null) { continue; } + if (s.Submarine.Info.IsRuin) { continue; } bool isCharacterInside = character.CurrentHull != null; bool isInnerWall = s.prefab.Tags.Contains("inner"); if (isInnerWall && !isCharacterInside) @@ -2425,11 +2696,24 @@ namespace Barotrauma continue; } } - if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } + if (aiTarget.Entity is Item targetItem) + { + if (targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } + if (targetParams.State == AIState.FleeTo) + { + float target = targetParams.Threshold; + if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) + { + target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin; + } + if (character.HealthPercentage > target) + { + continue; + } + } + } valueModifier *= targetParams.Priority; - if (valueModifier == 0.0f) { continue; } - if (targetingTag != "decoy") { if (SwarmBehavior != null && SwarmBehavior.Members.Any()) @@ -2449,7 +2733,7 @@ namespace Barotrauma { if (otherCharacter == character) { continue; } if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; } - if (!IsFriendly(character, otherCharacter)) { continue; } + if (!character.IsFriendly(otherCharacter)) { continue; } valueModifier /= 2; } } @@ -2463,17 +2747,30 @@ namespace Barotrauma { dist *= 0.9f; } - if (!CanPerceive(aiTarget, dist)) { continue; } + + if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget)) + { + continue; + } + + if (SelectedAiTarget == aiTarget) + { + // Stick to the current target + valueModifier *= 1.1f; + } //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); - - AITargetMemory targetMemory = GetTargetMemory(aiTarget, true); - if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y) + AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); + if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) { - // Inside the sub, treat objects that are up or down, as they were farther away. - dist *= 3; + float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y; + if (diff > 0) + { + // Inside the sub, treat objects that are up or down, as they were farther away. + dist *= MathHelper.Clamp(diff / 100, 2, 3); + } } if (targetParams.AttackPattern == AttackPattern.Circle) @@ -2493,6 +2790,12 @@ namespace Barotrauma } } + if (targetCharacter != null && Character.CurrentHull != null && Character.CurrentHull == targetCharacter.CurrentHull) + { + // In the same room with the target character + dist /= 2; + } + // Don't target characters that are outside of the allowed zone, unless chasing or escaping. switch (targetParams.State) { @@ -2527,7 +2830,7 @@ namespace Barotrauma // Don't target items that we own. // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) if (owner == character) { continue; } - if (owner != null && (IsFriendly(Character, owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) + if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget))) { continue; } @@ -2599,7 +2902,7 @@ namespace Barotrauma wall = wallTarget?.Structure; } // The target is not a wall or it's not the same as we are attached to -> release - bool releaseTarget = wall == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); + bool releaseTarget = wall?.Bodies == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); if (!releaseTarget) { for (int i = 0; i < wall.Sections.Length; i++) @@ -2645,6 +2948,7 @@ namespace Barotrauma wallTarget = null; if (SelectedAiTarget == null) { return; } if (SelectedAiTarget.Entity == null) { return; } + if (!canAttackWalls) { return; } if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; @@ -2847,10 +3151,15 @@ namespace Barotrauma { if (addIfNotFound) { - memory = new AITargetMemory(target, 10); + memory = new AITargetMemory(target, minPriority); targetMemories.Add(target, memory); } } + if (addIfNotFound) + { + // Keep the memory alive. + memory.Priority = Math.Max(memory.Priority, minPriority); + } return memory; } @@ -2862,7 +3171,7 @@ namespace Barotrauma { _selectedAiTarget = null; } - else if (CanPerceive(_selectedAiTarget)) + else if (CanPerceive(_selectedAiTarget, checkVisibility: false)) { var memory = GetTargetMemory(_selectedAiTarget, false); if (memory != null) @@ -3000,7 +3309,7 @@ namespace Barotrauma private void ResetParams(CharacterParams.TargetParams targetParams) { targetParams?.Reset(); - if (selectedTargetingParams == targetParams || State == AIState.Idle) + if (selectedTargetingParams == targetParams || State == AIState.Idle || State == AIState.Patrol) { ResetAITarget(); State = AIState.Idle; @@ -3014,7 +3323,7 @@ namespace Barotrauma { if (!onlyExisting && !tempParams.ContainsKey(tag)) { - if (AIParams.TryAddNewTarget(tag, state, priority ?? 100, out targetParams)) + if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) { tempParams.Add(tag, targetParams); } @@ -3051,6 +3360,7 @@ namespace Barotrauma ChangeParams(target.SpeciesName, state, priority); if (target.IsHuman) { + priority = GetTargetParams("human")?.Priority; // Target also items, because if we are blind and the target doesn't move, we can only perceive the target when it uses items if (state == AIState.Attack || state == AIState.Escape) { @@ -3061,20 +3371,19 @@ namespace Barotrauma { // If the target is shooting from the submarine, we might not perceive it because it doesn't move. // --> Target the submarine too. - if (target.Submarine != null && (canAttackDoors || canAttackWalls)) + if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls)) { - ChangeParams("room", state, priority); + ChangeParams("room", state, priority * 0.1f); if (canAttackWalls) { - ChangeParams("wall", state, priority); + ChangeParams("wall", state, priority * 0.1f); } if (canAttackDoors) { - ChangeParams("door", state, priority); + ChangeParams("door", state, priority * 0.1f); } } ChangeParams("provocative", state, priority, onlyExisting: true); - ChangeParams("light", state, priority, onlyExisting: true); } } } @@ -3100,12 +3409,16 @@ namespace Barotrauma protected override void OnStateChanged(AIState from, AIState to) { LatchOntoAI?.DeattachFromBody(reset: true); + if (disableTailCoroutine != null) + { + CoroutineManager.StopCoroutines(disableTailCoroutine); + Character.AnimController.RestoreTemporarilyDisabled(); + disableTailCoroutine = null; + } Character.AnimController.ReleaseStuckLimbs(); - escapeTarget = null; AttackingLimb = null; movementMargin = 0; - allGapsSearched = false; - unreachableGaps.Clear(); + ResetEscape(); if (isStateChanged && to == AIState.Idle && from != to) { SetStateResetTimer(); @@ -3117,11 +3430,16 @@ namespace Barotrauma private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing); - private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1) + private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false) { + bool insideSightRange; + bool insideSoundRange; + checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine; if (dist > 0) { - return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing; + insideSightRange = IsInRange(dist, target.SightRange, Sight); + if (!checkVisibility && insideSightRange) { return true; } + insideSoundRange = IsInRange(dist, target.SoundRange, Hearing); } else { @@ -3129,8 +3447,42 @@ namespace Barotrauma { distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition); } - return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2); + insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight); + if (!checkVisibility && insideSightRange) { return true; } + insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing); } + if (!checkVisibility) + { + return insideSightRange || insideSoundRange; + } + else + { + if (!insideSightRange && !insideSoundRange) { return false; } + // Inside the same submarine -> check whether the target is behind a wall + if (target.Entity is Character c && VisibleHulls.Contains(c.CurrentHull) || target.Entity is Item i && VisibleHulls.Contains(i.CurrentHull)) + { + return insideSightRange || insideSoundRange; + } + else + { + // No line of sight to the target -> Ignore sight and use only half of the sound range + if (dist > 0) + { + return IsInRange(dist, target.SoundRange, Hearing / 2); + } + else + { + if (distSquared < 0) + { + distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition); + } + return IsInRangeSqr(distSquared, target.SoundRange, Hearing / 2); + } + } + } + + bool IsInRange(float dist, float range, float perception) => dist <= range * perception; + bool IsInRangeSqr(float distSquared, float range, float perception) => distSquared <= MathUtils.Pow2(range * perception); } public void ReevaluateAttacks() @@ -3255,6 +3607,72 @@ namespace Barotrauma public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount); + public override bool Escape(float deltaTime) + { + if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)) + { + State = AIState.Idle; + return false; + } + else if (SelectedTargetMemory is AITargetMemory targetMemory && SelectedAiTarget?.Entity is Character) + { + targetMemory.Priority += deltaTime * PriorityFearIncrement; + } + bool isSteeringThroughGap = UpdateEscape(deltaTime, canAttackDoors); + if (!isSteeringThroughGap) + { + if (SelectedAiTarget?.Entity is Character targetCharacter && targetCharacter.CurrentHull == Character.CurrentHull) + { + SteerAwayFromTheEnemy(); + } + else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true)) + { + var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; + if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) + { + if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack) + { + SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority); + State = AIState.Attack; + } + } + } + } + if (EscapeTarget == null) + { + if (SelectedAiTarget?.Entity is Character) + { + SteerAwayFromTheEnemy(); + } + else + { + SteeringManager.SteeringWander(); + if (Character.CurrentHull == null) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + } + } + return isSteeringThroughGap; + + void SteerAwayFromTheEnemy() + { + if (SelectedAiTarget == null) { return; } + Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition); + if (Character.CurrentHull != null && !Character.AnimController.InWater) + { + // Inside + escapeDir = new Vector2(Math.Sign(escapeDir.X), 0); + } + if (!MathUtils.IsValid(escapeDir)) + { + escapeDir = Vector2.UnitY; + } + SteeringManager.Reset(); + SteeringManager.SteeringManual(deltaTime, escapeDir); + } + } + private readonly List targetLimbs = new List(); public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None) { @@ -3306,7 +3724,17 @@ namespace Barotrauma return null; } - public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public override void ServerWrite(IWriteMessage msg) + { + msg.Write((byte)State); + PetBehavior?.ServerWrite(msg); + } + + public override void ClientRead(IReadMessage msg) + { + State = (AIState)msg.ReadByte(); + PetBehavior?.ClientRead(msg); + } } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 8a152b9fc..eaaf5d744 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -20,6 +20,11 @@ namespace Barotrauma private float reactTimer; private float unreachableClearTimer; private bool shouldCrouch; + public bool IsInsideCave { get; private set; } + /// + /// Resets each frame + /// + public bool AutoFaceMovement = true; const float reactionTime = 0.3f; const float crouchRaycastInterval = 1; @@ -29,9 +34,6 @@ namespace Barotrauma private float flipTimer; private const float FlipInterval = 0.5f; - private float teamChangeTimer; - private const float TeamChangeInterval = 0.5f; - public const float HULL_SAFETY_THRESHOLD = 40; public const float HULL_LOW_OXYGEN_PERCENTAGE = 30; @@ -52,7 +54,7 @@ namespace Barotrauma private readonly float steeringBufferIncreaseSpeed = 100; private float steeringBuffer; - private readonly float obstacleRaycastInterval = 1; + private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5; private float obstacleRaycastTimer; private readonly float enemyCheckInterval = 0.2f; @@ -86,6 +88,8 @@ namespace Barotrauma private readonly SteeringManager outsideSteering, insideSteering; + public bool UseIndoorSteeringOutside { get; set; } = false; + public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; @@ -207,33 +211,78 @@ namespace Barotrauma IgnoredItems.Clear(); } - bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); + bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true) + { + Entity target = SelectedAiTarget?.Entity; + if (target == null) + { + return false; + } + if (useTargetSub) + { + if (target.Submarine is Submarine sub) + { + target = sub; + threshold += Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2; + } + else + { + return false; + } + } + return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2); + } + bool hasValidPath = HasValidPath(); if (Character.Submarine == null) { - if (hasValidPath) + // When the character is outside, far enough from the target, and the direct route is blocked, + // use the indoor steering with the main and side path waypoints to help avoid getting stuck in level walls + if (SelectedAiTarget?.Entity != null && !IsCloseEnoughToTarget(2000, useTargetSub: false)) { obstacleRaycastTimer -= deltaTime; if (obstacleRaycastTimer <= 0) { - obstacleRaycastTimer = obstacleRaycastInterval; - // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + obstacleRaycastTimer = obstacleRaycastIntervalLong; + Vector2 rayEnd = SelectedAiTarget.Entity.SimPosition; + if (SelectedAiTarget.Entity.Submarine != null) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + rayEnd += SelectedAiTarget.Entity.Submarine.SimPosition; + } + UseIndoorSteeringOutside = Submarine.PickBody(SimPosition, rayEnd, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null; + } + } + else + { + UseIndoorSteeringOutside = false; + if (hasValidPath) + { + obstacleRaycastTimer -= deltaTime; + if (obstacleRaycastTimer <= 0) + { + obstacleRaycastTimer = obstacleRaycastIntervalShort; + // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - PathSteering.CurrentPath.Unreachable = true; - break; + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + { + PathSteering.CurrentPath.Unreachable = true; + break; + } } } } } } + else + { + UseIndoorSteeringOutside = false; + } if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) { @@ -273,13 +322,31 @@ namespace Barotrauma } } } - if (Character.Submarine != null || hasValidPath && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) + + // Check whether the character is inside a cave + if (IsInsideCave) + { + // If the character was inside a cave, require them to move a bit further from the area to set the field back to false + // This is to avoid any twitchy behavior with the steering managers + IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => + { + var area = c.Area; + area.Inflate(new Vector2(100)); + return area.Contains(Character.WorldPosition); + }) is Level.Cave; + } + else + { + IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave; + } + + if (UseIndoorSteeringOutside || IsInsideCave || Character.CurrentHull?.Submarine != null || hasValidPath && IsCloseEnoughToTarget(maxSteeringBuffer) || IsCloseEnoughToTarget(steeringBuffer)) { if (steeringManager != insideSteering) { insideSteering.Reset(); + steeringManager = insideSteering; } - steeringManager = insideSteering; steeringBuffer += steeringBufferIncreaseSpeed * deltaTime; } else @@ -287,8 +354,8 @@ namespace Barotrauma if (steeringManager != outsideSteering) { outsideSteering.Reset(); + steeringManager = outsideSteering; } - steeringManager = outsideSteering; steeringBuffer = minSteeringBuffer; } steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); @@ -419,7 +486,7 @@ namespace Barotrauma Character.SelectedConstruction.SecondaryUse(deltaTime, Character); } } - else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) + else if (AutoFaceMovement && Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) { newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; } @@ -429,6 +496,7 @@ namespace Barotrauma flipTimer = FlipInterval; } } + AutoFaceMovement = true; MentalStateManager?.Update(deltaTime); ShipCommandManager?.Update(deltaTime); @@ -915,10 +983,10 @@ namespace Barotrauma return false; } - public static void ReportProblem(Character reporter, Order order) + public static void ReportProblem(Character reporter, Order order, Hull targetHull = null) { if (reporter == null || order == null) { return; } - var visibleHulls = new List(reporter.GetVisibleHulls()); + var visibleHulls = targetHull is null ? new List(reporter.GetVisibleHulls()) : new List { targetHull }; foreach (var hull in visibleHulls) { PropagateHullSafety(reporter, hull); @@ -965,16 +1033,19 @@ namespace Barotrauma } if (previousAttackResults.ContainsKey(attacker)) { - foreach (Affliction newAffliction in attackResult.Afflictions) + if (attackResult.Afflictions != null) { - var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); - if (matchingAffliction == null) + foreach (Affliction newAffliction in attackResult.Afflictions) { - previousAttackResults[attacker].Afflictions.Add(newAffliction); - } - else - { - matchingAffliction.Strength += newAffliction.Strength; + var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); + if (matchingAffliction == null) + { + previousAttackResults[attacker].Afflictions.Add(newAffliction); + } + else + { + matchingAffliction.Strength += newAffliction.Strength; + } } } previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb); @@ -991,9 +1062,12 @@ namespace Barotrauma float realDamage = attackResult.Damage; // including poisons etc float totalDamage = realDamage; - foreach (Affliction affliction in attackResult.Afflictions) + if (attackResult.Afflictions != null) { - totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + foreach (Affliction affliction in attackResult.Afflictions) + { + totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + } } if (totalDamage <= 0.01f) { return; } if (Character.IsBot) @@ -1047,7 +1121,7 @@ namespace Barotrauma { (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult); // Inform other NPCs - if (cumulativeDamage > 1) + if (cumulativeDamage > 1 || totalDamage >= 10) { InformOtherNPCs(cumulativeDamage); } @@ -1184,7 +1258,7 @@ namespace Barotrauma // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; } - if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) + if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk)) { cumulativeDamage = 100; } @@ -1240,10 +1314,7 @@ namespace Barotrauma { var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager) { - HoldPosition = - Character.Info?.Job?.Prefab.Identifier == "watchman" || - Character.CurrentHull == null || - Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective()?.Target is Character followTarget && followTarget.IsPlayer, + HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", AbortCondition = abortCondition, allowHoldFire = allowHoldFire, }; @@ -1293,6 +1364,8 @@ namespace Barotrauma ObjectiveManager.WaitTimer = waitDuration; } + public override bool Escape(float deltaTime) => UpdateEscape(deltaTime, canAttackDoors: false); + private void CheckCrouching(float deltaTime) { crouchRaycastTimer -= deltaTime; @@ -1415,7 +1488,7 @@ namespace Barotrauma if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss; + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } if (accumulatedDamage <= WarningThreshold) { return; } @@ -1510,7 +1583,7 @@ namespace Barotrauma var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss; + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); @@ -1971,13 +2044,13 @@ namespace Barotrauma if (c.Removed) { continue; } if (c.TeamID != Character.TeamID) { continue; } if (c.IsIncapacitated) { continue; } - other = c; if (c.IsPlayer) { if (c.SelectedConstruction == target.Item) { // If the other character is player, don't try to operate - return true; + other = c; + break; } } else if (c.AIController is HumanAIController operatingAI) @@ -1991,7 +2064,8 @@ namespace Barotrauma if (!isOrder && isTargetOrdered) { // If the other bot is ordered to operate the item, let him do it, unless we are ordered too - return true; + other = c; + break; } else { @@ -2012,18 +2086,20 @@ namespace Barotrauma // Steering is hard-coded -> cannot use the required skills collection defined in the xml if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) { - return true; + other = c; + break; } } else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c)) { - return true; + other = c; + break; } } } } } - return false; + return other != null; bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index cb8bdeff7..cf2635e9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -1,8 +1,8 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; using FarseerPhysics; namespace Barotrauma @@ -15,6 +15,11 @@ namespace Barotrauma private bool canOpenDoors; public bool CanBreakDoors { get; set; } + private bool ShouldBreakDoor(Door door) => + CanBreakDoors && + !door.Item.Indestructible && !door.Item.InvulnerableToDamage && + (door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID); + private Character character; private Vector2 currentTarget; @@ -23,7 +28,7 @@ namespace Barotrauma private float buttonPressCooldown; - const float ButtonPressInterval = 0.5f; + const float ButtonPressInterval = 0.25f; public SteeringPath CurrentPath { @@ -78,7 +83,7 @@ namespace Barotrauma public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host) { - pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), indoorsSteering: true); + pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true); pathFinder.GetNodePenalty = GetNodePenalty; this.canOpenDoors = canOpenDoors; @@ -111,9 +116,14 @@ namespace Barotrauma IsPathDirty = true; } - public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) + public void SteeringSeekSimple(Vector2 targetSimPos, float weight = 1) { - steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + steering += base.DoSteeringSeek(targetSimPos, weight); + } + + public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) + { + steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); } /// @@ -158,42 +168,47 @@ namespace Barotrauma } } - private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { - Vector2 targetDiff = target - currentTarget; - if (currentPath != null && currentPath.Nodes.Any()) + bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished; + if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) { - //current path calculated relative to a different sub than where the character is now - //take that into account when calculating if the target has moved - Submarine currentPathSub = currentPath?.Nodes.First().Submarine; - if (currentPathSub != character.Submarine && character.Submarine != null) + Vector2 targetDiff = target - currentTarget; + if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null) { - Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; - targetDiff += subDiff; + //target in a different sub than where the character is now + //take that into account when calculating if the target has moved + Submarine currentPathSub = currentPath?.CurrentNode?.Submarine; + if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; } + if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null) + { + Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; + targetDiff += subDiff; + } + } + if (targetDiff.LengthSquared() > 1) + { + needsNewPath = true; } } - bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1); //find a new path if one hasn't been found yet or the target is different from the current target if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; if (findPathTimer < 0) { + SkipCurrentPathNodes(); currentTarget = target; Vector2 currentPos = host.SimPosition; - if (character != null && character.Submarine == null) + pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; + pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; + var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); + bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; + if (newPath.Unreachable || newPath.Nodes.None()) { - var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false); - if (targetHull != null && targetHull.Submarine != null) - { - currentPos -= targetHull.Submarine.SimPosition; - } + useNewPath = false; } - pathFinder.InsideSubmarine = character.Submarine != null; - pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0; - var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); - bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; - if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) + else if (!useNewPath && currentPath != null && currentPath.CurrentNode != null) { // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). if (IsIdenticalPath()) @@ -205,7 +220,7 @@ namespace Barotrauma // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us). float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1); useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t); - if (!useNewPath) + if (!useNewPath && character.Submarine != null) { // It's possible that the current path was calculated from a start point that is no longer valid. // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. @@ -232,12 +247,42 @@ namespace Barotrauma } if (useNewPath) { + if (currentPath != null) + { + CheckDoorsInPath(); + } currentPath = newPath; } float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); findPathTimer = priority * Rand.Range(1.0f, 1.2f); IsPathDirty = false; return DiffToCurrentNode(); + + void SkipCurrentPathNodes() + { + if (!character.AnimController.InWater || character.Submarine != null) { return; } + if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; } + if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; } + // Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside. + // Do this to optimize the old path before creating and evaluating a new path. + // In general, this is to avoid behavior where: + // a) the character goes back to first reach CurrentNode when the second node would be closer; or + // b) the character moves along the path when they could cut through open space to reduce the total distance. + float pathDistance = Vector2.Distance(character.WorldPosition, CurrentPath.CurrentNode.WorldPosition); + pathDistance += CurrentPath.GetLength(startIndex: CurrentPath.CurrentIndex); + for (int i = CurrentPath.Nodes.Count - 1; i > CurrentPath.CurrentIndex + 1; i--) + { + var waypoint = CurrentPath.Nodes[i]; + float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition); + if (directDistance > (pathDistance * pathDistance) || Submarine.PickBody(host.SimPosition, waypoint.SimPosition, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + { + pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i); + continue; + } + CurrentPath.SkipToNode(i); + break; + } + } } } @@ -271,25 +316,35 @@ namespace Barotrauma pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; } return currentTarget - pos2; - } - if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f) + } + bool doorsChecked = false; + if (!character.LockHands && buttonPressCooldown <= 0.0f) { CheckDoorsInPath(); + doorsChecked = true; } Vector2 pos = host.SimPosition; - if (character != null && currentPath.CurrentNode != null) + if (character != null && CurrentPath.CurrentNode != null) { - if (CurrentPath.CurrentNode.Submarine != null) + var nodeSub = CurrentPath.CurrentNode.Submarine; + if (nodeSub != null) { if (character.Submarine == null) { - pos -= CurrentPath.CurrentNode.Submarine.SimPosition; + // Going inside + pos -= ConvertUnits.ToSimUnits(nodeSub.Position); } - else if (character.Submarine != currentPath.CurrentNode.Submarine) + else if (character.Submarine != nodeSub) { - pos -= ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position - character.Submarine.Position); + // Different subs + pos -= ConvertUnits.ToSimUnits(nodeSub.Position - character.Submarine.Position); } } + else if (character.Submarine != null) + { + // Going outside + pos += ConvertUnits.ToSimUnits(character.Submarine.Position); + } } bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders @@ -362,7 +417,7 @@ namespace Barotrauma } if (isAboveFloor || nextLadderSameAsCurrent) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } else if (nextLadder != null) @@ -372,7 +427,7 @@ namespace Barotrauma //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } return diff; @@ -394,7 +449,7 @@ namespace Barotrauma float distance = horizontalDistance + verticalDistance; if (ConvertUnits.ToSimUnits(distance) < targetDistance) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } } @@ -419,7 +474,7 @@ namespace Barotrauma float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed)) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } if (currentPath.CurrentNode == null) @@ -429,28 +484,51 @@ namespace Barotrauma return currentPath.CurrentNode.SimPosition - pos; } + private void NextNode(bool checkDoors) + { + if (checkDoors) + { + CheckDoorsInPath(); + } + currentPath.SkipToNextNode(); + } + private bool CanAccessDoor(Door door, Func buttonFilter = null) { - if (door.IsOpen || door.IsBroken) { return true; } - if (!door.Item.IsInteractable(character)) { return false; } - if (!CanBreakDoors) + if (door.IsBroken) { return true; } + if (!door.IsOpen) { - if (door.IsStuck || door.IsJammed) { return false; } - if (!canOpenDoors || character.LockHands) { return false; } + if (!door.Item.IsInteractable(character)) { return false; } + if (!ShouldBreakDoor(door)) + { + if (door.IsStuck || door.IsJammed) { return false; } + if (!canOpenDoors || character.LockHands) { return false; } + } } if (door.HasIntegratedButtons) { - return door.HasAccess(character) || CanBreakDoors; + return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door); } else { - return door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors; + // We'll want this to run each time, because the delegate is used to find a valid button component. + bool canAccessButtons = door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); } } + private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()); + + private float GetColliderLength() + { + Vector2 colliderSize = character.AnimController.Collider.GetSize(); + return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y)); + } + private void CheckDoorsInPath() { - for (int i = 0; i < 2; i++) + if (!canOpenDoors) { return; } + for (int i = 0; i < 5; i++) { WayPoint currentWaypoint = null; WayPoint nextWaypoint = null; @@ -461,17 +539,21 @@ namespace Barotrauma { door = currentPath.Nodes.First().ConnectedDoor; shouldBeOpen = door != null; + if (i > 0) { break; } } else { - if (i == 0) + bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed; + if (i == 0 || !closeDoors) { currentWaypoint = currentPath.CurrentNode; nextWaypoint = currentPath.NextNode; } else { - currentWaypoint = currentPath.PrevNode; + int previousIndex = currentPath.CurrentIndex - i; + if (previousIndex < 0) { break; } + currentWaypoint = currentPath.Nodes[previousIndex]; nextWaypoint = currentPath.CurrentNode; } if (currentWaypoint?.ConnectedDoor == null) { continue; } @@ -480,24 +562,30 @@ namespace Barotrauma { //the node we're heading towards is the last one in the path, and at a door //the door needs to be open for the character to reach the node - if (currentWaypoint.ConnectedDoor.LinkedGap != null && currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom) + if (currentWaypoint.ConnectedDoor.LinkedGap != null) { - shouldBeOpen = true; - door = currentWaypoint.ConnectedDoor; + // Keep the airlock doors closed, but not in ruins/wrecks + if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom || currentWaypoint.Submarine?.Info.IsRuin != null || currentWaypoint.Submarine?.Info.IsWreck != null) + { + shouldBeOpen = true; + door = currentWaypoint.ConnectedDoor; + } } } else { + float colliderLength = GetColliderLength(); door = currentWaypoint.ConnectedDoor; if (door.LinkedGap.IsHorizontal) { int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); - shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f; + float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X; + shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size; } else { int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y); - shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f; + shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength; } } } @@ -541,7 +629,7 @@ namespace Barotrauma } else if (closestButton != null) { - if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2)) + if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2)) { closestButton.Item.TryInteract(character, false, true); buttonPressCooldown = ButtonPressInterval; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 9ffdcf361..69270a61e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -19,16 +19,19 @@ namespace Barotrauma private Body targetBody; private Vector2 attachSurfaceNormal; private Submarine targetSubmarine; + private Character targetCharacter; private readonly Character character; public bool AttachToSub { get; private set; } public bool AttachToWalls { get; private set; } + public bool AttachToCharacters { get; private set; } - private readonly float minDeattachSpeed, maxDeattachSpeed; + private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown; private readonly float damageOnDetach, detachStun; - private float deattachTimer; + private readonly bool weld; + private float deattachCheckTimer; - private Vector2 wallAttachPos; + private Vector2 _attachPos; private float attachCooldown; @@ -38,9 +41,9 @@ namespace Barotrauma private float jointDir; - public List AttachJoints { get; } = new List(); + public List AttachJoints { get; } = new List(); - public Vector2? WallAttachPos + public Vector2? AttachPos { get; private set; @@ -48,18 +51,22 @@ namespace Barotrauma public bool IsAttached => AttachJoints.Count > 0; - public bool IsAttachedToSub => IsAttached && targetSubmarine != null; + public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null; 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); string limbString = element.GetAttributeString("attachlimb", null); attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); @@ -81,30 +88,54 @@ namespace Barotrauma public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal) { + if (!AttachToSub) { return; } if (wall == null) { return; } var sub = wall.Submarine; if (sub == null) { return; } + Reset(); targetWall = wall; targetSubmarine = sub; targetBody = targetSubmarine.PhysicsBody.FarseerBody; this.attachSurfaceNormal = attachSurfaceNormal; - wallAttachPos = attachPos; + _attachPos = attachPos; + } + + public void SetAttachTarget(Character target) + { + if (!AttachToCharacters) { return; } + Reset(); + targetCharacter = target; + targetSubmarine = target.Submarine; + targetBody = target.AnimController.Collider.FarseerBody; + attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } public void Update(EnemyAIController enemyAI, float deltaTime) { if (character.Submarine != null) { - DeattachFromBody(reset: true); - return; + if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine || + character.Submarine != null && targetSubmarine != null && targetCharacter == null) + { + DeattachFromBody(reset: true); + return; + } } - if (AttachJoints.Count > 0) + if (IsAttached) { if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { - AttachJoints[0].LocalAnchorA = - new Vector2(-AttachJoints[0].LocalAnchorA.X, AttachJoints[0].LocalAnchorA.Y); - AttachJoints[0].ReferenceAngle = -AttachJoints[0].ReferenceAngle; + var attachJoint = AttachJoints[0]; + if (attachJoint is WeldJoint weldJoint) + { + weldJoint.LocalAnchorA = new Vector2(-weldJoint.LocalAnchorA.X, weldJoint.LocalAnchorA.Y); + weldJoint.ReferenceAngle = -weldJoint.ReferenceAngle; + } + else if (attachJoint is RevoluteJoint revoluteJoint) + { + revoluteJoint.LocalAnchorA = new Vector2(-revoluteJoint.LocalAnchorA.X, revoluteJoint.LocalAnchorA.Y); + revoluteJoint.ReferenceAngle = -revoluteJoint.ReferenceAngle; + } jointDir = attachLimb.Dir; } for (int i = 0; i < AttachJoints.Count; i++) @@ -113,31 +144,51 @@ namespace Barotrauma if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f) { #if DEBUG - DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); + DebugConsole.Log("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); #endif DeattachFromBody(reset: true); return; } } + if (targetCharacter != null) + { + if (enemyAI.AttackingLimb?.attack == null) + { + DeattachFromBody(reset: true, cooldown: 1); + } + else + { + float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + { + DeattachFromBody(reset: true, cooldown: 1); + } + } + } } if (attachCooldown > 0) { attachCooldown -= deltaTime; } - if (deattachTimer > 0) + if (deattachCheckTimer > 0) { - deattachTimer -= deltaTime; + deattachCheckTimer -= deltaTime; } - Vector2 transformedAttachPos = wallAttachPos; + if (targetCharacter != null) + { + // Own sim pos -> target where we are + _attachPos = character.SimPosition; + } + Vector2 transformedAttachPos = _attachPos; if (character.Submarine == null && targetSubmarine != null) { transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { - WallAttachPos = transformedAttachPos; + AttachPos = transformedAttachPos; } switch (enemyAI.State) @@ -151,7 +202,7 @@ namespace Barotrauma //check if there are any walls nearby the character could attach to if (raycastTimer < 0.0f) { - wallAttachPos = Vector2.Zero; + _attachPos = Vector2.Zero; var cells = Level.Loaded.GetCells(character.WorldPosition, 1); if (cells.Count > 0) @@ -169,7 +220,7 @@ namespace Barotrauma { attachSurfaceNormal = edge.GetNormal(cell); targetBody = cell.Body; - wallAttachPos = potentialAttachPos; + _attachPos = potentialAttachPos; closestDist = distSqr; } break; @@ -183,21 +234,20 @@ namespace Barotrauma } else { - wallAttachPos = Vector2.Zero; + _attachPos = Vector2.Zero; } - - if (wallAttachPos == Vector2.Zero || targetBody == null) + if (_attachPos == Vector2.Zero || targetBody == null) { DeattachFromBody(reset: false); } else { - float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + 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; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach - AttachToBody(wallAttachPos); + AttachToBody(_attachPos); enemyAI.SteeringManager.Reset(); } else @@ -205,25 +255,22 @@ namespace Barotrauma //move closer to the wall DeattachFromBody(reset: false); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f); - enemyAI.SteeringManager.SteeringSeek(wallAttachPos); + enemyAI.SteeringManager.SteeringSeek(_attachPos); } } break; case AIState.Attack: case AIState.Aggressive: - if (enemyAI.AttackingLimb != null) + if (enemyAI.IsSteeringThroughGap) { break; } + if (_attachPos == Vector2.Zero) { break; } + if (!AttachToSub && !AttachToCharacters) { break; } + if (enemyAI.AttackingLimb == null) { break; } + if (targetBody == null) { break; } + if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } + Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { - if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null) - { - // is not attached or is attached to something else - if (!IsAttached || IsAttached && AttachJoints[0].BodyB != targetBody) - { - if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) - { - AttachToBody(transformedAttachPos); - } - } - } + AttachToBody(transformedAttachPos); } break; default: @@ -231,43 +278,51 @@ namespace Barotrauma break; } - if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f) + if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f) { bool deattach = false; - // Deattach if the wall is broken enough where we are attached to - int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); - if (enemyAI.CanPassThroughHole(targetWall, targetSection)) + if (maxAttachDuration > 0) { deattach = true; - attachCooldown = 2; + attachCooldown = coolDown; } - if (!deattach) + if (!deattach && targetWall != null && targetSubmarine != null) { - // Deattach if the velocity is high - float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); - deattach = velocity > maxDeattachSpeed; + // Deattach if the wall is broken enough where we are attached to + int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); + if (enemyAI.CanPassThroughHole(targetWall, targetSection)) + { + deattach = true; + attachCooldown = coolDown; + } if (!deattach) { - if (velocity > minDeattachSpeed) + // Deattach if the velocity is high + float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); + deattach = velocity > maxDeattachSpeed; + if (!deattach) { - float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? - Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : - (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); - - if (Rand.Range(0.0f, 1.0f) < velocityFactor) + if (velocity > minDeattachSpeed) { - deattach = true; - character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); - attachCooldown = detachStun * 2; + float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? + Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : + (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); + + if (Rand.Range(0.0f, 1.0f) < velocityFactor) + { + deattach = true; + character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); + attachCooldown = Math.Max(detachStun * 2, coolDown); + } } } } + deattachCheckTimer = 5.0f; } if (deattach) { DeattachFromBody(reset: true); } - deattachTimer = 5.0f; } } @@ -315,16 +370,30 @@ namespace Barotrauma } collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2); - var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) - { - FrequencyHz = 10.0f, - DampingRatio = 0.5f, - KinematicBodyB = true, - CollideConnected = false, - //Length = 0.1f - }; + Joint colliderJoint = weld ? + new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) + { + FrequencyHz = 10.0f, + DampingRatio = 0.5f, + KinematicBodyB = true, + CollideConnected = false, + } : + new RevoluteJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) + { + MotorEnabled = true, + MaxMotorTorque = 0.25f + } as Joint; + GameMain.World.Add(colliderJoint); - AttachJoints.Add(colliderJoint); + AttachJoints.Add(colliderJoint); + if (targetCharacter != null) + { + targetCharacter.Latchers.Add(this); + } + if (maxAttachDuration > 0) + { + deattachCheckTimer = maxAttachDuration; + } } public void DeattachFromBody(bool reset, float cooldown = 0) @@ -342,14 +411,23 @@ namespace Barotrauma { Reset(); } + if (targetCharacter != null) + { + targetCharacter.Latchers.Remove(this); + } } private void Reset() { + if (targetCharacter != null) + { + targetCharacter.Latchers.Remove(this); + } + targetCharacter = null; targetWall = null; targetSubmarine = null; targetBody = null; - WallAttachPos = null; + AttachPos = null; } private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 66376cc1d..23bfc48ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -94,8 +94,9 @@ namespace Barotrauma if (_abandon) { #if DEBUG - if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder()) + if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder() && !objectiveManager.IsCurrentOrder()) { + // TODO: dismiss throw new Exception("Order abandoned!"); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index e2ca2f781..42950891c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -83,12 +83,13 @@ namespace Barotrauma if (suitableContainer != null) { bool equip = item.GetComponent() != null || - item.AllowedSlots.None(s => - s == InvSlotType.Card || - s == InvSlotType.Head || - s == InvSlotType.Headset || - s == InvSlotType.InnerClothes || - s == InvSlotType.OuterClothes); + item.AllowedSlots.Any(s => s != InvSlotType.Any) && + item.AllowedSlots.None(s => + s == InvSlotType.Card || + s == InvSlotType.Head || + s == InvSlotType.Headset || + s == InvSlotType.InnerClothes || + s == InvSlotType.OuterClothes); TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index fa7112563..6bdddd863 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -101,17 +101,17 @@ namespace Barotrauma public enum CombatMode { - Defensive, - Offensive, - Arrest, - Retreat, - None + 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 } public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; - private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious; + private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f); private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; @@ -252,9 +252,40 @@ namespace Barotrauma { case CombatMode.Offensive: case CombatMode.Arrest: - Engage(); + Engage(deltaTime); break; case CombatMode.Defensive: + if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder()) + { + if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200) + { + Engage(deltaTime); + } + else + { + // Keep following the goto target + var gotoObjective = objectiveManager.GetOrder(); + if (gotoObjective != null) + { + gotoObjective.ForceAct(deltaTime); + if (!character.AnimController.InWater) + { + HumanAIController.FaceTarget(Enemy); + ForceWalk = true; + HumanAIController.AutoFaceMovement = false; + } + } + else + { + SteeringManager.Reset(); + } + } + } + else + { + Retreat(deltaTime); + } + break; case CombatMode.Retreat: Retreat(deltaTime); break; @@ -588,8 +619,9 @@ namespace Barotrauma // assume that it's required for the stun effect // as we can't check the status effect conditions here. var mobileBatteryTag = "mobilebattery"; - var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && - container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag)))); + var containers = weapon.Item.Components.Where(ic => + ic is ItemContainer container && + container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); // If there's no such container, assume that the melee weapon can stun without a battery. return containers.None() || containers.Any(container => (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); @@ -670,6 +702,14 @@ namespace Barotrauma { RemoveSubObjective(ref retreatObjective); } + if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance)) + { + // Swim away + SteeringManager.Reset(); + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition)); + SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); + return; + } if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) { if (findHullTimer > 0) @@ -684,7 +724,10 @@ namespace Barotrauma } if (retreatTarget != null && character.CurrentHull != retreatTarget) { - TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true), + TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true) + { + UsePathingOutside = false + }, onAbandon: () => { if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull)) @@ -703,7 +746,7 @@ namespace Barotrauma } } - private void Engage() + private void Engage(float deltaTime) { if (WeaponComponent == null) { @@ -721,6 +764,21 @@ namespace Barotrauma RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref seekAmmunitionObjective); RemoveSubObjective(ref seekWeaponObjective); + if (character.Submarine == null && WeaponComponent is MeleeWeapon meleeWeapon) + { + if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range)) + { + // Swim towards the target + SteeringManager.Reset(); + SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10); + SteeringManager.SteeringAvoid(deltaTime, 5, weight: 15); + } + else + { + SteeringManager.Reset(); + } + return; + } if (followTargetObjective != null && followTargetObjective.Target != Enemy) { RemoveFollowTarget(); @@ -728,6 +786,7 @@ namespace Barotrauma TryAddSubObjective(ref followTargetObjective, constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50) { + UsePathingOutside = false, IgnoreIfTargetDead = true, DialogueIdentifier = "dialogcannotreachtarget", TargetName = Enemy.DisplayName, @@ -958,14 +1017,15 @@ namespace Barotrauma } if (reloadTimer > 0) { return; } if (holdFireCondition != null && holdFireCondition()) { return; } - float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position); + sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); + distanceTimer = distanceCheckInterval; if (WeaponComponent is MeleeWeapon meleeWeapon) { bool closeEnough = true; float sqrRange = meleeWeapon.Range * meleeWeapon.Range; if (character.AnimController.InWater) { - if (sqrDist > sqrRange) + if (sqrDistance > sqrRange) { closeEnough = false; } @@ -992,6 +1052,7 @@ namespace Barotrauma if (closeEnough) { UseWeapon(deltaTime); + character.AIController.SteeringManager.Reset(); } else if (!character.IsFacing(Enemy.WorldPosition)) { @@ -1003,7 +1064,7 @@ namespace Barotrauma { if (WeaponComponent is RepairTool repairTool) { - if (sqrDist > repairTool.Range * repairTool.Range) { return; } + if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index b6163032a..251c0362b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -53,9 +53,13 @@ namespace Barotrauma distanceFactor = 1; } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); - if (severity > 0.5f && !isOrder) + if (severity > 0.75f && !isOrder && + targetHull.RoomName != null && + !targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) && + !targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) && + !targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase)) { - // Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead). + // Ignore severe fires to prevent casualities unless ordered to extinguish. Priority = 0; Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 62787477d..97d9450c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -25,7 +25,7 @@ namespace Barotrauma /// /// 0-1 based on the horizontal size of all of the fires in the hull. /// - public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X))); + public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X))); protected override IEnumerable GetList() => Hull.hullList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 14c01cb47..6e13c261d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -56,13 +56,15 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character) { if (target == null || target.Removed) { return false; } - if (target.IsDead || target.IsUnconscious) { return false; } + if (target.IsDead) { return false; } + if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; } if (target == character) { return false; } if (target.Submarine == null) { return false; } if (character.Submarine == null) { return false; } if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } + if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 76c51c0d9..c42d0c6f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -39,6 +39,10 @@ namespace Barotrauma return; } targetItem = character.Inventory.FindItemByTag(gearTag, true); + if (targetItem == null && gearTag == LIGHT_DIVING_GEAR) + { + targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true); + } if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) { TryAddSubObjective(ref getDivingGear, () => @@ -57,23 +61,37 @@ namespace Barotrauma }; }, onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref getDivingGear)); + onCompleted: () => + { + RemoveSubObjective(ref getDivingGear); + if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) + { + foreach (Item mask in masks) + { + if (mask != targetItem) + { + character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot); + } + } + } + }); } else { - // Seek oxygen that has at least 10% condition left, if we are inside a friendly sub. - // The margin helps us to survive, because we might need some oxygen before we can find more oxygen. - // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. - float min = character.Submarine != Submarine.MainSub ? 0.01f : MIN_OXYGEN; + float min = GetMinOxygen(character); if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) { TryAddSubObjective(ref getOxygen, () => { if (character.IsOnPlayerTeam) { - if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min)) + if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) + { + character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f); + } } else { @@ -105,7 +123,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f)) + if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) { character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); } @@ -121,7 +139,7 @@ namespace Barotrauma int ReportOxygenTankCount() { if (character.Submarine != Submarine.MainSub) { return 1; } - int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); if (remainingOxygenTanks == 0) { character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); @@ -136,17 +154,6 @@ namespace Barotrauma } } - /// - /// Returns false only when no inventory can be found from the item. - /// - public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable containedItems) - { - containedItems = target.OwnInventory?.AllItems; - if (containedItems == null) { return false; } - AIController.UnequipEmptyItems(actor, target); - return true; - } - public override void Reset() { base.Reset(); @@ -154,5 +161,20 @@ namespace Barotrauma getOxygen = null; targetItem = null; } + + public static float GetMinOxygen(Character character) + { + // Seek oxygen that has at least 10% condition left, if we are inside a friendly sub. + // The margin helps us to survive, because we might need some oxygen before we can find more oxygen. + // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. + float min = 0.01f; + float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min; + if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen)) + { + // There's a valid oxygen tank in the inventory -> no need to swap the tank too early. + minOxygen = min; + } + return minOxygen; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e582044fb..2c8f6a40b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,20 +46,25 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = (objectiveManager.IsCurrentOrder() || objectiveManager.HasActiveObjective()) && HumanAIController.HasDivingSuit(character) ? 0 : 100; + Priority = (objectiveManager.IsCurrentOrder() || + objectiveManager.IsCurrentOrder() || + objectiveManager.Objectives.Any(o => o.Priority > 0 && o is AIObjectiveCombat)) + && HumanAIController.HasDivingSuit(character) ? 0 : 100; } else { if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && (needsSuit ? - !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) : - !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN))) + !HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) : + !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))) { Priority = 100; } - else if (objectiveManager.IsCurrentOrder() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + else if ((objectiveManager.IsCurrentOrder() || objectiveManager.IsCurrentOrder()) && + character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) { - // Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear + // Ordered to follow, hold position, or return back to main sub inside a hostile sub + // -> ignore find safety unless we need to find a diving gear Priority = 0; } Priority = MathHelper.Clamp(Priority, 0, 100); @@ -126,11 +131,11 @@ namespace Barotrauma bool needsEquipment = false; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character)); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.GetMinOxygen(character)); } if (needsEquipment) { @@ -298,17 +303,21 @@ namespace Barotrauma Hull bestHull = null; float bestValue = 0; + bool bestIsAirlock = false; foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h))) { if (hull.Submarine == null) { continue; } + // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. + if (hull.Submarine.Info.IsRuin) { continue; } if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; } if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } float hullSafety = 0; - if (character.CurrentHull != null && character.Submarine != null) + bool hullIsAirlock = false; + bool isCharacterInside = character.CurrentHull != null && character.Submarine != null; + if (isCharacterInside) { - // Inside if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character); float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); @@ -325,7 +334,7 @@ namespace Barotrauma continue; } // Don't allow to go outside if not already outside. - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null); if (path.Unreachable) { HumanAIController.UnreachableHulls.Add(hull); @@ -343,24 +352,16 @@ namespace Barotrauma } else { - // Outside - if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) + // TODO: could also target gaps that get us inside? + if (hull.IsTaggedAirlock()) + { + hullSafety = 100; + hullIsAirlock = true; + } + else if(!bestIsAirlock && hull.LeadsOutside(character)) { hullSafety = 100; } - else - { - // TODO: could also target gaps that get us inside? - foreach (Item item in Item.ItemList) - { - if (item.CurrentHull != hull && item.HasTag("airlock")) - { - hullSafety = 100; - break; - } - } - } - // TODO: could we get a closest door to the outside and target the flowing hull if no airlock is found? // Huge preference for closer targets float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); @@ -372,10 +373,11 @@ namespace Barotrauma hullSafety /= 10; } } - if (hullSafety > bestValue) + if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock)) { bestHull = hull; bestValue = hullSafety; + bestIsAirlock = hullIsAirlock; } } return bestHull; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 870b10333..dd98bf747 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -323,7 +323,7 @@ namespace Barotrauma // This is relatively expensive, so let's do this only when it significantly improves the behavior. // Only allow one path find call per frame. hasCalledPathFinder = true; - var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); if (path.Unreachable) { continue; } } currItemPriority = itemPriority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 099c63df4..a67611486 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -27,6 +27,7 @@ namespace Barotrauma public bool followControlledCharacter; public bool mimic; public bool SpeakIfFails { get; set; } = true; + public bool UsePathingOutside { get; set; } = true; public float extraDistanceWhileSwimming; public float extraDistanceOutsideSub; @@ -121,13 +122,14 @@ namespace Barotrauma } private readonly float avoidLookAheadDistance = 5; + private readonly float pathWaitingTime = 3; public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) : base(character, objectiveManager, priorityModifier) { Target = target; this.repeat = repeat; - waitUntilPathUnreachable = 3.0f; + waitUntilPathUnreachable = pathWaitingTime; this.getDivingGearIfNeeded = getDivingGearIfNeeded; if (Target is Item i) { @@ -159,6 +161,8 @@ namespace Barotrauma } } + public void ForceAct(float deltaTime) => Act(deltaTime); + protected override void Act(float deltaTime) { if (followControlledCharacter) @@ -184,7 +188,6 @@ namespace Barotrauma // Wait character.AIController.SteeringManager.Reset(); } - waitUntilPathUnreachable -= deltaTime; if (!character.IsClimbing) { character.SelectedConstruction = null; @@ -220,11 +223,13 @@ namespace Barotrauma { Abandon = true; } - else if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty) + else if (HumanAIController.IsCurrentPathUnreachable) { + waitUntilPathUnreachable -= deltaTime; SteeringManager.Reset(); if (waitUntilPathUnreachable < 0) { + waitUntilPathUnreachable = pathWaitingTime; if (repeat) { SpeakCannotReach(); @@ -240,7 +245,7 @@ namespace Barotrauma if (getDivingGearIfNeeded && !character.LockHands) { Character followTarget = Target as Character; - bool needsDivingSuit = targetIsOutside; + bool needsDivingSuit = !isInside || targetIsOutside; bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); if (mimic) { @@ -255,7 +260,7 @@ namespace Barotrauma } } bool needsEquipment = false; - float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN; + float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); if (needsDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); @@ -323,25 +328,29 @@ namespace Barotrauma } else { - SeekGaps(maxGapDistance); - seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); - if (TargetGap != null) + bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null; + if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true)) { - // Check that nothing is blocking the way - Vector2 rayStart = character.SimPosition; - Vector2 rayEnd = TargetGap.SimPosition; - if (TargetGap.Submarine != null && character.Submarine == null) + SeekGaps(maxGapDistance); + seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); + if (TargetGap != null) { - rayStart -= TargetGap.Submarine.SimPosition; - } - else if (TargetGap.Submarine == null && character.Submarine != null) - { - rayEnd -= character.Submarine.SimPosition; - } - var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); - if (closestBody != null) - { - TargetGap = null; + // Check that nothing is blocking the way + Vector2 rayStart = character.SimPosition; + Vector2 rayEnd = TargetGap.SimPosition; + if (TargetGap.Submarine != null && character.Submarine == null) + { + rayStart -= TargetGap.Submarine.SimPosition; + } + else if (TargetGap.Submarine == null && character.Submarine != null) + { + rayEnd -= character.Submarine.SimPosition; + } + var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); + if (closestBody != null) + { + TargetGap = null; + } } } } @@ -365,7 +374,7 @@ namespace Barotrauma if (checkScooterTimer <= 0) { useScooter = false; - checkScooterTimer = checkScooterTime; + checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); string scooterTag = "scooter"; string batteryTag = "mobilebattery"; Item scooter = null; @@ -444,18 +453,33 @@ namespace Barotrauma } if (SteeringManager == PathSteering) { + Vector2 targetPos = character.GetRelativeSimPosition(Target); Func nodeFilter = null; if (isInside && !AllowGoingOutside) { nodeFilter = n => n.Waypoint.CurrentHull != null; } + else if (!isInside && HumanAIController.UseIndoorSteeringOutside) + { + nodeFilter = n => n.Waypoint.Submarine == null; + } - PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, - startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), - endNodeFilter, - nodeFilter, - CheckVisibility); - + if (!isInside && !UsePathingOutside) + { + PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10); + if (character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); + } + } + else + { + PathSteering.SteeringSeek(targetPos, weight: 1, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), + endNodeFilter: endNodeFilter, + nodeFilter: nodeFilter, + checkVisiblity: CheckVisibility); + } if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { if (useScooter) @@ -501,9 +525,22 @@ namespace Barotrauma { character.CursorPosition -= character.Submarine.Position; } - Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position); - if (!MathUtils.IsValid(dir)) { dir = Vector2.UnitY; } - SteeringManager.SteeringManual(1.0f, dir); + Vector2 diff = character.CursorPosition - character.Position; + Vector2 dir = Vector2.Normalize(diff); + float sqrDist = diff.LengthSquared(); + if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) + { + SteeringManager.SteeringManual(1.0f, dir); + } + else + { + float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2)); + bool isFacing = dot > 0.9f; + if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough)) + { + SteeringManager.SteeringManual(1.0f, dir); + } + } character.SetInput(InputType.Aim, false, true); character.SetInput(InputType.Shoot, false, true); } @@ -511,7 +548,7 @@ namespace Barotrauma private bool useScooter; private float checkScooterTimer; - private readonly float checkScooterTime = 0.2f; + private readonly float checkScooterTime = 0.5f; public Hull GetTargetHull() => GetTargetHull(Target); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 1b7b68af7..4be1b47fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -242,9 +242,8 @@ namespace Barotrauma if (!searchingNewHull) { //find all available hulls first - FindTargetHulls(); searchingNewHull = true; - return; + FindTargetHulls(); } else if (targetHulls.Any()) { @@ -252,14 +251,13 @@ namespace Barotrauma currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID; bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node => + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } - // Check that there is no unsafe or forbidden hulls on the way to the target + // Check that there is no unsafe hulls on the way to the target if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } - if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; } return true; - }); + }, endNodeFilter: node => !isCurrentHullAllowed | !IsForbidden(node.Waypoint.CurrentHull)); if (path.Unreachable) { //can't go to this room, remove it from the list and try another room @@ -271,31 +269,20 @@ namespace Barotrauma SetTargetTimerLow(); return; } + character.AIController.SelectTarget(currentTarget.AiTarget); + PathSteering.SetPath(path); + SetTargetTimerNormal(); searchingNewHull = false; } else { - // Couldn't find a target for some reason -> reset + // Couldn't find a valid hull SetTargetTimerHigh(); searchingNewHull = false; } - - if (currentTarget != null) - { - character.AIController.SelectTarget(currentTarget.AiTarget); - string errorMsg = null; -#if DEBUG - bool isRoomNameFound = currentTarget.DisplayName != null; - errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")"; -#endif - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null); - PathSteering.SetPath(path); - } - SetTargetTimerNormal(); } newTargetTimer -= deltaTime; - - if (!character.IsClimbing && IsSteeringFinished()) + if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished())) { Wander(deltaTime); } @@ -406,9 +393,10 @@ namespace Barotrauma hullWeights.Clear(); foreach (var hull in Hull.hullList) { + if (character.Submarine == null) { break; } if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } - if (character.Submarine == null) { break; } + if (hull.Submarine.Info.IsRuin || hull.Submarine.Info.IsWreck) { continue; } if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) { if (hull.Submarine.TeamID != character.TeamID) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 6c868993d..4cb710919 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -163,7 +163,7 @@ namespace Barotrauma CoroutineManager.StopCoroutines(coroutine); DelayedObjectives.Remove(objective); } - coroutine = CoroutineManager.InvokeAfter(() => + coroutine = CoroutineManager.Invoke(() => { //round ended before the coroutine finished #if CLIENT @@ -233,11 +233,7 @@ namespace Barotrauma if (orderObjective == null) { return; } #if DEBUG // Note: don't automatically remove orders here. Removing orders needs to be done via dismissing. - if (orderObjective.IsCompleted) - { - DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); - } - else if (!orderObjective.CanBeCompleted) + if (!orderObjective.CanBeCompleted) { DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red); } @@ -281,9 +277,9 @@ namespace Barotrauma ForcedOrder?.CalculatePriority(); AIObjective orderWithHighestPriority = null; float highestPriority = 0; - foreach (var currentOrder in CurrentOrders) + for (int i = CurrentOrders.Count - 1; i >= 0; i--) { - var orderObjective = currentOrder.Objective; + var orderObjective = CurrentOrders[i].Objective; if (orderObjective == null) { continue; } orderObjective.CalculatePriority(); if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority) @@ -467,6 +463,11 @@ namespace Barotrauma AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine) }; break; + case "return": + newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier); + newObjective.Abandoned += () => DismissSelf(order, option); + newObjective.Completed += () => DismissSelf(order, option); + break; case "fixleaks": newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); break; @@ -586,6 +587,27 @@ namespace Barotrauma return newObjective; } + private void DismissSelf(Order order, string option) + { + var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option)); + if (currentOrder.Order == null) + { +#if DEBUG + DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found"); +#endif + return; + } +#if CLIENT + if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) + { + GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character); + } +#else + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, currentOrder.Order?.TargetSpatialEntity, character, character)); +#endif + } + + private bool IsAllowedToWait() { if (!character.IsOnPlayerTeam) { return false; } @@ -606,6 +628,8 @@ namespace Barotrauma public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T).Objective as T; + /// /// Returns the last active objective of the specific type. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index e9e310f16..66cc99a4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -408,7 +408,7 @@ namespace Barotrauma } bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) || - targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold); + targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 73115b133..48936b9f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -83,7 +83,7 @@ namespace Barotrauma if (character.AIController is HumanAIController humanAI) { if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) || - target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold)) + target.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs new file mode 100644 index 000000000..78d80ac98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -0,0 +1,273 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Barotrauma +{ + class AIObjectiveReturn : AIObjective + { + public override string Identifier { get; set; } = "return"; + private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective; + private bool usingEscapeBehavior; + private bool isSteeringThroughGap; + public Submarine ReturnTarget { get; } + + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) + { + ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); + if (ReturnTarget == null) + { + DebugConsole.ThrowError("Error with a Return objective: no suitable return target found"); + Abandon = true; + } + + Submarine GetReturnTarget(IEnumerable subs) + { + var requiredTeamID = orderGiver?.TeamID ?? character?.TeamID; + Submarine returnTarget = null; + foreach (var sub in subs) + { + if (sub == null) { continue; } + if (sub.TeamID != requiredTeamID) { continue; } + returnTarget = sub; + break; + } + return returnTarget; + } + } + + protected override float GetPriority() + { + if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this)) + { + Priority = objectiveManager.GetOrderPriority(this); + } + else + { + // TODO: Consider if this needs to be addressed + Priority = 0; + } + return Priority; + } + + protected override void Act(float deltaTime) + { + if (ReturnTarget == null) + { + Abandon = true; + return; + } + bool shouldUseEscapeBehavior = false; + if (character.CurrentHull != null || isSteeringThroughGap) + { + if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget)) + { + // Character is on another sub that is not connected to the target sub, use the escape behavior to get them out + shouldUseEscapeBehavior = true; + if (!usingEscapeBehavior) + { + HumanAIController.ResetEscape(); + } + isSteeringThroughGap = HumanAIController.Escape(deltaTime); + if (!isSteeringThroughGap && (HumanAIController.EscapeTarget == null || HumanAIController.IsCurrentPathUnreachable)) + { + Abandon = true; + } + } + else if (character.Submarine != ReturnTarget) + { + // Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub + if (moveInsideObjective == null) + { + Hull targetHull = null; + foreach (var d in ReturnTarget.ConnectedDockingPorts.Values) + { + if (!d.Docked) { continue; } + if (d.DockingTarget == null) { continue; } + if (d.DockingTarget.Item.Submarine != character.Submarine) { continue; } + targetHull = d.Item.CurrentHull; + break; + } + if (targetHull != null && !targetHull.IsTaggedAirlock()) + { + // Target the closest airlock + float closestDist = 0; + Hull airlock = null; + foreach (Hull hull in Hull.hullList) + { + if (hull.Submarine != targetHull.Submarine) { continue; } + if (!hull.IsTaggedAirlock()) { continue; } + float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position); + if (airlock == null || closestDist <= 0 || dist < closestDist) + { + airlock = hull; + closestDist = dist; + } + + } + if (airlock != null) + { + targetHull = airlock; + } + } + if (targetHull != null) + { + RemoveSubObjective(ref moveInCaveObjective); + RemoveSubObjective(ref moveOutsideObjective); + TryAddSubObjective(ref moveInsideObjective, + constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager) + { + AllowGoingOutside = true, + endNodeFilter = n => n.Waypoint.Submarine == targetHull.Submarine + }, + onCompleted: () => RemoveSubObjective(ref moveInsideObjective), + onAbandon: () => Abandon = true); + } + else + { +#if DEBUG + DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveInsideObjective'"); +#endif + } + } + } + else + { + // Character is on the target sub, the objective is completed + IsCompleted = true; + } + } + else if (!isSteeringThroughGap && moveInCaveObjective == null && moveOutsideObjective == null) + { + if (HumanAIController.IsInsideCave) + { + WayPoint closestOutsideWaypoint = null; + float closestDistance = float.MaxValue; + foreach (var w in WayPoint.WayPointList) + { + if (w.Tunnel != null && w.Tunnel.Type == Level.TunnelType.Cave) { continue; } + if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; } + float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition); + if (closestOutsideWaypoint == null || distance < closestDistance) + { + closestOutsideWaypoint = w; + closestDistance = distance; + } + } + if (closestOutsideWaypoint != null) + { + RemoveSubObjective(ref moveInsideObjective); + RemoveSubObjective(ref moveOutsideObjective); + TryAddSubObjective(ref moveInCaveObjective, + constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager) + { + endNodeFilter = n => n.Waypoint == closestOutsideWaypoint, + AllowGoingOutside = true + }, + onCompleted: () => RemoveSubObjective(ref moveInCaveObjective), + onAbandon: () => Abandon = true); + } + else + { +#if DEBUG + DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'"); +#endif + } + } + else + { + Hull targetHull = null; + float targetDistanceSquared = float.MaxValue; + bool targetIsAirlock = false; + foreach (var hull in ReturnTarget.GetHulls(false)) + { + bool hullIsAirlock = hull.IsTaggedAirlock(); + if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character))) + { + float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition); + if (targetHull == null || distanceSquared < targetDistanceSquared) + { + targetHull = hull; + targetDistanceSquared = distanceSquared; + targetIsAirlock = hullIsAirlock; + } + } + } + if (targetHull != null) + { + RemoveSubObjective(ref moveInsideObjective); + RemoveSubObjective(ref moveInCaveObjective); + TryAddSubObjective(ref moveOutsideObjective, + constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager) + { + AllowGoingOutside = true + }, + onCompleted: () => RemoveSubObjective(ref moveOutsideObjective), + onAbandon: () => Abandon = true); + } + else + { +#if DEBUG + DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'"); +#endif + } + } + } + else + { + if (HumanAIController.IsInsideCave) + { + RemoveSubObjective(ref moveOutsideObjective); + } + else + { + RemoveSubObjective(ref moveInCaveObjective); + } + } + usingEscapeBehavior = shouldUseEscapeBehavior; + } + + protected override bool CheckObjectiveSpecific() + { + if (IsCompleted) + { + return true; + } + if (ReturnTarget == null) + { + Abandon = true; + return false; + } + if (character.Submarine == ReturnTarget) + { + IsCompleted = true; + } + return IsCompleted; + } + + public override void Reset() + { + base.Reset(); + moveInsideObjective = null; + moveInCaveObjective = null; + moveOutsideObjective = null; + usingEscapeBehavior = false; + isSteeringThroughGap = false; + HumanAIController.ResetEscape(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + SteeringManager?.Reset(); + if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) + { + string msg = TextManager.Get("dialogcannotreturn", returnNull: true); + if (msg != null) + { + character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 09a73221f..57b024e16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -141,6 +141,7 @@ namespace Barotrauma public Entity TargetEntity; public ItemComponent TargetItemComponent; public readonly bool UseController; + public readonly string[] ControllerTags; public Controller ConnectedController; public Character OrderGiver; @@ -309,6 +310,7 @@ namespace Barotrauma color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); + ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); @@ -380,6 +382,7 @@ namespace Barotrauma SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; UseController = prefab.UseController; + ControllerTags = prefab.ControllerTags; TargetAllCharacters = prefab.TargetAllCharacters; AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; @@ -399,7 +402,7 @@ namespace Barotrauma { if (UseController) { - ConnectedController = targetItem.Item?.FindController(); + ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); if (ConnectedController == null) { DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); @@ -450,19 +453,37 @@ namespace Barotrauma return false; } - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "") + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null) { - orderOption ??= ""; - - string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier; - if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } - - if (targetCharacterName == null) { targetCharacterName = ""; } - if (targetRoomName == null) { targetRoomName = ""; } - string msg = TextManager.GetWithVariables(messageTag, new string[2] { "[name]", "[roomname]" }, new string[2] { targetCharacterName, targetRoomName }, new bool[2] { false, true }, true); - if (msg == null) { return ""; } - - return msg; + priority ??= CharacterInfo.HighestManualOrderPriority; + // If the order has a lesser priority, it means we are rearranging character orders + if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed") + { + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + } + string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}"; + messageTag += $".{Identifier}"; + if (!string.IsNullOrEmpty(orderOption)) + { + if (Identifier != "dismissed") + { + messageTag += $".{orderOption}"; + } + else + { + string[] splitOption = orderOption.Split('.'); + if (splitOption.Length > 0) + { + messageTag += $".{splitOption[0]}"; + } + } + } + string msg = TextManager.GetWithVariables(messageTag, + new string[2] { "[name]", "[roomname]" }, + new string[2] { targetCharacterName ?? string.Empty, targetRoomName ?? string.Empty }, + formatCapitals: new bool[2] { false, true }, + returnNull: true); + return msg ?? string.Empty; } /// @@ -505,7 +526,7 @@ namespace Barotrauma if (item.NonInteractable) { continue; } if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; } Controller controller = null; - if (UseController && !item.TryFindController(out controller)) { continue; } + if (UseController && !item.TryFindController(out controller, tags: ControllerTags)) { continue; } if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; } matchingItems.Add(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 82b4ed49b..e46839e24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.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; @@ -78,6 +79,31 @@ namespace Barotrauma return nodeList; } + + private bool? blocked; + public bool IsBlocked() + { + if (blocked.HasValue) { return blocked.Value; } + + blocked = false; + + if (Waypoint.Submarine != null) { return blocked.Value; } + if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; } + foreach (var w in Level.Loaded.ExtraWalls) + { + if (!(w is DestructibleLevelWall d)) { return blocked.Value; } + if (d.Destroyed) { return blocked.Value; } + if (!d.IsPointInside(Waypoint.Position)) { return blocked.Value; } + blocked = true; + break; + } + return blocked.Value; + } + + public void ResetBlocked() + { + blocked = null; + } } class PathFinder @@ -86,96 +112,116 @@ namespace Barotrauma public GetNodePenaltyHandler GetNodePenalty; private readonly List nodes; - public readonly bool IndoorsSteering; + private readonly bool isCharacter; public bool InsideSubmarine { get; set; } public bool ApplyPenaltyToOutsideNodes { get; set; } - public PathFinder(List wayPoints, bool indoorsSteering = false) + public PathFinder(List wayPoints, bool isCharacter) { - nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering), removeOrphans: true); - + var filtered = isCharacter ? wayPoints : wayPoints.FindAll(w => w.Submarine == null); + nodes = PathNode.GenerateNodes(filtered, removeOrphans: true); foreach (WayPoint wp in wayPoints) { - wp.linkedTo.CollectionChanged += WaypointLinksChanged; + wp.OnLinksChanged += WaypointLinksChanged; } - - IndoorsSteering = indoorsSteering; + this.isCharacter = isCharacter; } - void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + void WaypointLinksChanged(WayPoint wp) { if (Submarine.Unloading) { return; } - var waypoints = sender as IEnumerable; + var node = nodes.Find(n => n.Waypoint == wp); + if (node == null) { return; } - foreach (MapEntity me in waypoints) + for (int i = node.connections.Count - 1; i >= 0; i--) { - WayPoint wp = me as WayPoint; - if (me == null) { continue; } - - var node = nodes.Find(n => n.Waypoint == wp); - if (node == null) { return; } - - if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) + //remove connection if the waypoint isn't connected anymore + if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null) { - for (int i = node.connections.Count - 1; i >= 0; i--) - { - //remove connection if the waypoint isn't connected anymore - if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null) - { - node.connections.RemoveAt(i); - node.distances.RemoveAt(i); - } - } + node.connections.RemoveAt(i); + node.distances.RemoveAt(i); } - else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) + } + + for (int i = 0; i < wp.linkedTo.Count; i++) + { + if (!(wp.linkedTo[i] is WayPoint connected)) { continue; } + + //already connected, continue + if (node.connections.Any(n => n.Waypoint == connected)) { continue; } + + var matchingNode = nodes.Find(n => n.Waypoint == connected); + if (matchingNode == null) { - for (int i = 0; i < wp.linkedTo.Count; i++) - { - if (!(wp.linkedTo[i] is WayPoint connected)) { continue; } - - //already connected, continue - if (node.connections.Any(n => n.Waypoint == connected)) { continue; } - - var matchingNode = nodes.Find(n => n.Waypoint == connected); - if (matchingNode == null) - { #if DEBUG - DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder"); + DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder"); #endif - return; - } - - node.connections.Add(matchingNode); - node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position)); - } + return; } + + node.connections.Add(matchingNode); + node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position)); } } - private static readonly List sortedNodes = new List(); + private readonly List sortedNodes = new List(); - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { + foreach (PathNode node in nodes) + { + node.ResetBlocked(); + } + //sort nodes roughly according to distance sortedNodes.Clear(); foreach (PathNode node in nodes) { node.TempPosition = node.Position; - if (hostSub != null) + var wpSub = node.Waypoint.Submarine; + if (hostSub != null && wpSub == null) { - Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition; - node.TempPosition -= diff; + // inside and targeting outside + node.TempPosition -= hostSub.SimPosition; + } + else if (wpSub != null && hostSub != null && wpSub != hostSub) + { + // different subs + node.TempPosition -= hostSub.SimPosition - wpSub.SimPosition; + } + else if (hostSub == null && wpSub != null) + { + // Outside and targeting inside + node.TempPosition += wpSub.SimPosition; } float xDiff = Math.Abs(start.X - node.TempPosition.X); float yDiff = Math.Abs(start.Y - node.TempPosition.Y); - if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null) { yDiff += 10.0f; } - node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub + if (InsideSubmarine && !(node.Waypoint.Submarine?.Info?.IsRuin ?? false)) + { + //higher cost for vertical movement when inside the sub + if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null) + { + yDiff += 10.0f; + } + node.TempDistance = xDiff + yDiff * 10.0f; + } + else + { + node.TempDistance = xDiff + yDiff; + } //much higher cost to waypoints that are outside if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; } + //optimization: + //node extremely far, don't try to use it as a start node + if (node.TempDistance > 800.0f) + { + continue; + } + //prefer nodes that are closer to the end position node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f; @@ -187,31 +233,39 @@ namespace Barotrauma sortedNodes.Insert(i, node); } + bool IsWaypointVisible(PathNode node, Vector2 rayStart, bool checkVisibility = true) + { + //if searching for a path inside the sub, make sure the waypoint is visible + if (checkVisibility && isCharacter) + { + if (node.Waypoint.isObstructed) { return false; } + var body = Submarine.PickBody(rayStart, node.TempPosition, + collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); + if (body != null) + { + if (body.UserData is Submarine) { return false; } + if (body.UserData is Structure s && !s.IsPlatform) { return false; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; } + } + } + return true; + } + //find the most suitable start node, starting from the ones that are the closest PathNode startNode = null; foreach (PathNode node in sortedNodes) { - if (startNode == null || node.TempDistance < startNode.TempDistance) + if (nodeFilter != null && !nodeFilter(node)) { continue; } + if (startNodeFilter != null && !startNodeFilter(node)) { continue; } + // Always check the visibility for the start node + if (!IsWaypointVisible(node, start)) { continue; } + if (node.IsBlocked()) { continue; } + if (node.Waypoint.ConnectedGap != null) { - if (nodeFilter != null && !nodeFilter(node)) { continue; } - if (startNodeFilter != null && !startNodeFilter(node)) { continue; } - //if searching for a path inside the sub, make sure the waypoint is visible - if (IndoorsSteering) - { - if (node.Waypoint.isObstructed) { continue; } - - // Always check the visibility for the start node - var body = Submarine.PickBody( - start, node.TempPosition, null, - Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); - if (body != null) - { - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } - } - } - startNode = node; + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } } + startNode = node; + break; } if (startNode == null) @@ -252,28 +306,17 @@ namespace Barotrauma PathNode endNode = null; foreach (PathNode node in sortedNodes) { - if (endNode == null || node.TempDistance < endNode.TempDistance) + if (nodeFilter != null && !nodeFilter(node)) { continue; } + if (endNodeFilter != null && !endNodeFilter(node)) { continue; } + // Only check the visibility for the end node when allowed (fix leaks) + if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; } + if (node.IsBlocked()) { continue; } + if (node.Waypoint.ConnectedGap != null) { - if (nodeFilter != null && !nodeFilter(node)) { continue; } - if (endNodeFilter != null && !endNodeFilter(node)) { continue; } - if (IndoorsSteering) - { - if (node.Waypoint.isObstructed) { continue; } - //if searching for a path inside the sub, make sure the waypoint is visible - if (checkVisibility) - { - // Only check the visibility for the end node when allowed (fix leaks) - var body = Submarine.PickBody(end, node.TempPosition, null, - Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); - if (body != null) - { - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } - } - } - } - endNode = node; + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } } + endNode = node; + break; } if (endNode == null) @@ -284,40 +327,12 @@ namespace Barotrauma return new SteeringPath(true); } - var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr); + var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); return path; } - public SteeringPath FindPath(WayPoint start, WayPoint end) - { - PathNode startNode = null, endNode = null; - foreach (PathNode node in nodes) - { - if (node.Waypoint == start) - { - startNode = node; - if (endNode != null) { break; } - } - if (node.Waypoint == end) - { - endNode = node; - if (startNode != null) { break; } - } - } - - if (startNode == null || endNode == null) - { -#if DEBUG - DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed); -#endif - return new SteeringPath(true); - } - - return FindPath(startNode, endNode); - } - - private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "") + private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0) { if (start == end) { @@ -342,14 +357,16 @@ namespace Barotrauma float dist = float.MaxValue; foreach (PathNode node in nodes) { - if (node.state != 1) { continue; } - if (IndoorsSteering && node.Waypoint.isObstructed) { continue; } + if (node.state != 1 || node.F > dist) { continue; } + if (isCharacter && node.Waypoint.isObstructed) { continue; } if (filter != null && !filter(node)) { continue; } - if (node.F < dist) + if (node.IsBlocked()) { continue; } + if (node.Waypoint.ConnectedGap != null) { - dist = node.F; - currNode = node; - } + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + } + dist = node.F; + currNode = node; } if (currNode == null || currNode == end) { break; } @@ -451,6 +468,8 @@ namespace Barotrauma return path; } + + private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index 780ec45e4..d9039091d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -57,7 +57,10 @@ namespace Barotrauma public void SteeringManual(float deltaTime, Vector2 velocity) { - steering += velocity; + if (MathUtils.IsValid(velocity)) + { + steering += velocity; + } } public void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs index 79c8dbefa..24a3feb2b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; namespace Barotrauma @@ -24,16 +25,47 @@ namespace Barotrauma if (Unreachable) { return float.PositiveInfinity; } if (!totalLength.HasValue) { - totalLength = 0.0f; - for (int i = 0; i < nodes.Count - 1; i++) - { - totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition); - } + CalculateTotalLength(); } return totalLength.Value; } } + public float GetLength(int? startIndex = null, int? endIndex = null) + { + if (Unreachable) { return float.PositiveInfinity; } + startIndex ??= 0; + endIndex ??= Nodes.Count - 1; + if (startIndex == 0 && endIndex == Nodes.Count - 1) + { + return TotalLength; + } + if (!totalLength.HasValue) + { + CalculateTotalLength(); + } + float length = 0.0f; + for (int i = startIndex.Value; i < endIndex.Value; i++) + { + length += nodeDistances[i]; + } + return length; + } + + private void CalculateTotalLength() + { + totalLength = 0.0f; + nodeDistances.Clear(); + for (int i = 0; i < nodes.Count - 1; i++) + { + float distance = Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition); + totalLength += distance; + nodeDistances.Add(distance); + } + } + + private readonly List nodeDistances = new List(); + public SteeringPath(bool unreachable = false) { nodes = new List(); @@ -107,6 +139,11 @@ namespace Barotrauma currentIndex++; } + public void SkipToNode(int nodeIndex) + { + currentIndex = nodeIndex; + } + public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f) { if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 3d5ac5a49..dc6e181ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -161,8 +161,8 @@ namespace Barotrauma for (int i = 0; i < container.Inventory.Capacity; i++) { if (container.Inventory.GetItemAt(i) != null) { continue; } - if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && - Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && + Config.ForbiddenAmmunition.None(id => id.Equals(ip.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) { Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 702670425..bbaba7ba8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -1,12 +1,31 @@ -using FarseerPhysics; +using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { abstract class AnimController : Ragdoll { + public Vector2 RightHandIKPos { get; protected set; } + public Vector2 LeftHandIKPos { get; protected set; } + + protected LimbJoint rightShoulder, leftShoulder; + protected float upperArmLength, forearmLength; + protected float useItemTimer; + protected bool aiming; + protected bool wasAiming; + protected bool aimingMelee; + protected bool wasAimingMelee; + + public bool IsAiming => wasAiming; + public bool IsAimingMelee => wasAimingMelee; + + public float ArmLength => upperArmLength + forearmLength; + public abstract GroundedMovementParams WalkParams { get; set; } public abstract GroundedMovementParams RunParams { get; set; } public abstract SwimParams SwimSlowParams { get; set; } @@ -42,6 +61,10 @@ namespace Barotrauma } else { + if (this is HumanoidAnimController humanAnimController && humanAnimController.Crouching) + { + return humanAnimController.HumanCrouchParams; + } return IsMovingFast ? RunParams : WalkParams; } } @@ -56,14 +79,14 @@ namespace Barotrauma } else { - return IsMovingFast? SwimFastParams : SwimSlowParams; + return IsMovingFast ? SwimFastParams : SwimSlowParams; } } } public bool CanWalk => RagdollParams.CanWalk; public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); - + // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; @@ -96,7 +119,12 @@ namespace Barotrauma { if (CanWalk) { - return new List { WalkParams, RunParams, SwimSlowParams, SwimFastParams }; + var anims = new List { WalkParams, RunParams, SwimSlowParams, SwimFastParams }; + if (this is HumanoidAnimController humanAnimController) + { + anims.Add(humanAnimController.HumanCrouchParams); + } + return anims; } else { @@ -146,15 +174,11 @@ namespace Barotrauma public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } - public virtual void UpdateAnim(float deltaTime) { } + public abstract void UpdateAnim(float deltaTime); - public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { } + public abstract void DragCharacter(Character target, float deltaTime); - public virtual void DragCharacter(Character target, float deltaTime) { } - - public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { } - - public float GetSpeed(AnimationType type) + public virtual float GetSpeed(AnimationType type) { GroundedMovementParams movementParams; switch (type) @@ -207,7 +231,14 @@ namespace Barotrauma } else { - animType = AnimationType.Walk; + if (this is HumanoidAnimController humanAnimController && humanAnimController.Crouching) + { + animType = AnimationType.Crouch; + } + else + { + animType = AnimationType.Walk; + } } } return GetSpeed(animType); @@ -221,6 +252,12 @@ namespace Barotrauma return WalkParams; case AnimationType.Run: return RunParams; + case AnimationType.Crouch: + if (this is HumanoidAnimController humanAnimController) + { + return humanAnimController.HumanCrouchParams; + } + throw new NotImplementedException(type.ToString()); case AnimationType.SwimSlow: return SwimSlowParams; case AnimationType.SwimFast: @@ -231,5 +268,459 @@ namespace Barotrauma throw new NotImplementedException(type.ToString()); } } + + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) + { + useItemTimer = 0.5f; + Anim = Animation.UsingConstruction; + + if (!allowMovement) + { + TargetMovement = Vector2.Zero; + TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; + float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); + if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + { + TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); + } + } + + if (!character.Enabled) { return; } + + Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos); + if (character.Submarine != null) + { + handSimPos -= character.Submarine.SimPosition; + } + + var leftHand = GetLimb(LimbType.LeftHand); + if (leftHand != null) + { + leftHand.Disabled = true; + leftHand.PullJointEnabled = true; + leftHand.PullJointWorldAnchorB = handSimPos; + } + + var rightHand = GetLimb(LimbType.RightHand); + if (rightHand != null) + { + rightHand.Disabled = true; + rightHand.PullJointEnabled = true; + rightHand.PullJointWorldAnchorB = handSimPos; + } + } + + public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) + { + for (int i = 0; i < 2; i++) + { + Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand); + + pullLimb.Disabled = true; + + pullLimb.PullJointEnabled = true; + pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos; + pullLimb.PullJointMaxForce = 500.0f; + } + } + + 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) + { + aimingMelee = aimMelee; + if (character.Stun > 0.0f || character.IsIncapacitated) + { + aim = false; + } + + //calculate the handle positions + Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); + float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale); + + //handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f); + //handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f); + + transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom); + transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom); + + Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; + Limb leftHand = GetLimb(LimbType.LeftHand); + Limb rightHand = GetLimb(LimbType.RightHand); + + Vector2 itemPos = aim ? aimPos : holdPos; + + var controller = character.SelectedConstruction?.GetComponent(); + bool usingController = controller != null && !controller.AllowAiming; + bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; + float itemAngle; + Holdable holdable = item.GetComponent(); + float torsoRotation = torso.Rotation; + + Item rightHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); + bool equippedInRightHand = rightHandItem == item && rightHand != null && !rightHand.IsSevered; + Item leftHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); + bool equippedInLefthand = leftHandItem == item && leftHand != null && !leftHand.IsSevered; + 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 : Vector2.UnitX; + holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; + holdAngle += GetAimWobble(rightHand, leftHand, item); + itemAngle = torsoRotation + holdAngle * Dir; + + 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); + if (!anotherItemControlsPose) + { + var head = GetLimb(LimbType.Head); + if (head != null) + { + head.body.SmoothRotate(itemAngle, force: 30 * head.Mass); + } + if (TargetMovement == Vector2.Zero && inWater) + { + torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; + torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); + } + } + aiming = true; + } + } + else + { + if (holdable.UseHandRotationForHoldAngle) + { + if (equippedInRightHand) + { + itemAngle = rightHand.Rotation + holdAngle * Dir; + } + else if (equippedInLefthand) + { + itemAngle = leftHand.Rotation + holdAngle * Dir; + } + else + { + itemAngle = torsoRotation + holdAngle * Dir; + } + } + else + { + itemAngle = torsoRotation + holdAngle * Dir; + } + } + + if (rightShoulder == null) { return; } + Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; + if (itemPos == Vector2.Zero || isClimbing || usingController) + { + if (equippedInRightHand) + { + transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; + itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; + } + else if (equippedInLefthand) + { + transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; + itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; + } + } + else + { + if (equippedInRightHand) + { + transformedHoldPos = rightShoulder.WorldAnchorA; + rightHand.Disabled = true; + } + if (equippedInLefthand) + { + if (leftShoulder == null) { return; } + transformedHoldPos = leftShoulder.WorldAnchorA; + leftHand.Disabled = true; + } + itemPos.X *= Dir; + transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle)); + } + + item.body.ResetDynamics(); + + Vector2 currItemPos = equippedInRightHand ? + rightHand.PullJointWorldAnchorA - transformedHandlePos[0] : + leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; + + if (!MathUtils.IsValid(currItemPos)) + { + string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " + + currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA + + ", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] + + ", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] + + ", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle + + ", collider pos: " + character.SimPosition; + DebugConsole.Log(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + errorMsg); + + return; + } + + if (holdable.Pusher != null) + { + if (character.Stun > 0.0f || character.IsIncapacitated) + { + holdable.Pusher.Enabled = false; + } + else + { + if (!holdable.Pusher.Enabled) + { + holdable.Pusher.Enabled = true; + holdable.Pusher.ResetDynamics(); + holdable.Pusher.SetTransform(currItemPos, itemAngle); + } + else + { + holdable.Pusher.TargetPosition = currItemPos; + holdable.Pusher.TargetRotation = holdAngle * Dir; + + holdable.Pusher.MoveToTargetPosition(true); + + currItemPos = holdable.Pusher.SimPosition; + itemAngle = holdable.Pusher.Rotation; + } + } + } + float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); + float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); + float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); + if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) + { + itemRotation = targetAngle; + } + item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); + previousDirection = dir; + + if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle)) + { + for (int i = 0; i < 2; i++) + { + if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; } +#if DEBUG + if (handlePos[i].LengthSquared() > ArmLength) + { + DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)"); + } +#endif + HandIK( + i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], + CurrentAnimationParams.ArmIKStrength, + CurrentAnimationParams.HandIKStrength, + maxAngularVelocity: 15.0f); + } + } + } + + private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem) + { + float wobbleStrength = 0.0f; + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + } + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + } + if (wobbleStrength <= 0.1f) { return 0.0f; } + wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); + + float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f; + float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f; + + return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; + } + + public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f, float maxAngularVelocity = float.PositiveInfinity) + { + Vector2 shoulderPos; + + Limb arm, forearm; + if (hand.type == LimbType.LeftHand) + { + if (leftShoulder == null) { return; } + shoulderPos = leftShoulder.WorldAnchorA; + arm = GetLimb(LimbType.LeftArm); + forearm = GetLimb(LimbType.LeftForearm); + LeftHandIKPos = pos; + } + else + { + if (rightShoulder == null) { return; } + shoulderPos = rightShoulder.WorldAnchorA; + arm = GetLimb(LimbType.RightArm); + forearm = GetLimb(LimbType.RightForearm); + RightHandIKPos = pos; + } + if (arm == null) { return; } + + //distance from shoulder to holdpos + float c = Vector2.Distance(pos, shoulderPos); + c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f); + + float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + arm.Params.GetSpriteOrientation() - MathHelper.PiOver2; + float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir; + float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir; + + //make sure the arm angle "has the same number of revolutions" as the arm + while (arm.Rotation - armAngle > MathHelper.Pi) + { + armAngle += MathHelper.TwoPi; + } + while (arm.Rotation - armAngle < -MathHelper.Pi) + { + armAngle -= MathHelper.TwoPi; + } + + if (arm?.body != null && Math.Abs(arm.body.AngularVelocity) < maxAngularVelocity) + { + arm.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false); + } + float forearmAngle = armAngle + lowerArmAngle; + if (forearm?.body != null && Math.Abs(forearm.body.AngularVelocity) < maxAngularVelocity) + { + forearm.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false); + } + if (hand?.body != null && Math.Abs(hand.body.AngularVelocity) < maxAngularVelocity) + { + float handAngle = forearm != null ? forearmAngle : armAngle; + hand.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false); + } + } + + public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10) + { + var leftHand = GetLimb(LimbType.LeftHand); + var rightHand = GetLimb(LimbType.RightHand); + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + if (waist == null) { return; } + Vector2 midPos = waist.SimPosition; + if (leftHand != null) + { + leftHand.Disabled = true; + leftHandPos.X *= Dir; + leftHandPos += midPos; + HandIK(leftHand, leftHandPos); + } + if (rightHand != null) + { + rightHand.Disabled = true; + rightHandPos.X *= Dir; + rightHandPos += midPos; + HandIK(rightHand, rightHandPos); + } + var leftFoot = GetLimb(LimbType.LeftFoot); + if (leftFoot != null) + { + leftFoot.Disabled = true; + leftFootPos = new Vector2(waist.SimPosition.X + leftFootPos.X * Dir, GetColliderBottom().Y + leftFootPos.Y); + MoveLimb(leftFoot, leftFootPos, Math.Abs(leftFoot.SimPosition.X - leftFootPos.X) * footMoveForce * leftFoot.Mass, true); + } + var rightFoot = GetLimb(LimbType.RightFoot); + if (rightFoot != null) + { + rightFoot.Disabled = true; + rightFootPos = new Vector2(waist.SimPosition.X + rightFootPos.X * Dir, GetColliderBottom().Y + rightFootPos.Y); + MoveLimb(rightFoot, rightFootPos, Math.Abs(rightFoot.SimPosition.X - rightFootPos.X) * footMoveForce * rightFoot.Mass, true); + } + } + + public void ApplyTestPose() + { + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + if (waist != null) + { + ApplyPose( + new Vector2(-0.75f, -0.2f), + new Vector2(0.75f, -0.2f), + new Vector2(-WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale), + new Vector2(WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale)); + } + } + + protected void CalculateArmLengths() + { + //calculate arm and forearm length (atm this assumes that both arms are the same size) + Limb rightForearm = GetLimb(LimbType.RightForearm); + Limb rightHand = GetLimb(LimbType.RightHand); + if (rightHand == null) { return; } + + rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ?? GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm }); + leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ?? GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm }); + + Vector2 localAnchorShoulder = Vector2.Zero; + Vector2 localAnchorElbow = Vector2.Zero; + if (rightShoulder != null) + { + localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB; + } + LimbJoint rightElbow = rightForearm == null ? + GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) : + GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm); + if (rightElbow != null) + { + localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB; + } + upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow); + if (rightElbow != null) + { + if (rightForearm == null) + { + forearmLength = Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + } + else + { + LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); + if (rightWrist != null) + { + forearmLength = Vector2.Distance( + rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, + rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); + + forearmLength += Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightWrist.LimbA.type == LimbType.RightHand ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); + } + } + } + } + + protected LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB) + { + return LimbJoints.FirstOrDefault(lj => + (lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) || + (lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB)); + } + + protected LimbJoint GetJoint(LimbType matchingType, IEnumerable ignoredTypes) + { + return LimbJoints.FirstOrDefault(lj => + lj.LimbA.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t) || + lj.LimbB.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t)); + } + + public override void Recreate(RagdollParams ragdollParams = null) + { + base.Recreate(ragdollParams); + if (Character.Params.CanInteract) + { + CalculateArmLengths(); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 0ca3c9c1c..c37d0d8b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -97,9 +97,9 @@ namespace Barotrauma public new FishSwimParams CurrentSwimParams => base.CurrentSwimParams as FishSwimParams; public float? TailAngle => GetValidOrNull(CurrentAnimationParams, CurrentFishAnimation?.TailAngleInRadians); - public float FootTorque => CurrentFishAnimation.FootTorque; - public float HeadTorque => CurrentFishAnimation.HeadTorque; - public float TorsoTorque => CurrentFishAnimation.TorsoTorque; + public float FootTorque => CurrentAnimationParams.FootTorque; + public float HeadTorque => CurrentAnimationParams.HeadTorque; + public float TorsoTorque => CurrentAnimationParams.TorsoTorque; public float TailTorque => CurrentFishAnimation.TailTorque; public float HeadMoveForce => CurrentGroundedParams.HeadMoveForce; public float TorsoMoveForce => CurrentGroundedParams.TorsoMoveForce; @@ -139,7 +139,7 @@ namespace Barotrauma if (MainLimb == null) { return; } var mainLimb = MainLimb; - levitatingCollider = true; + levitatingCollider = !IsHanging; if (!character.CanMove) { @@ -192,6 +192,11 @@ namespace Barotrauma strongestImpact = 0.0f; } + if (aiming) + { + TargetMovement = TargetMovement.ClampLength(2); + } + if (inWater && !forceStanding) { Collider.FarseerBody.FixedRotation = false; @@ -202,7 +207,7 @@ namespace Barotrauma if (CurrentGroundedParams != null) { //rotate collider back upright - float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians; + float standAngle = CurrentGroundedParams.ColliderStandAngleInRadians * Dir; if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f) { Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f; @@ -215,16 +220,19 @@ namespace Barotrauma } UpdateWalkAnim(deltaTime); } - if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); + return; + } + if (character.AnimController.AnimationTestPose) + { + ApplyTestPose(); } - //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { return; } - if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip)) + if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !aiming) { if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { @@ -289,6 +297,10 @@ namespace Barotrauma { flipTimer = 0.0f; } + wasAiming = aiming; + aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; } private bool CanDrag(Character target) @@ -362,7 +374,7 @@ namespace Barotrauma { //pull the character's mouth to the target character (again with a fluctuating force) float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f)); - mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength); } else { @@ -389,11 +401,17 @@ namespace Barotrauma } if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) { - bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; + static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; //keep severing joints until there is only one limb left var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); if (nonSeveredJoints.None()) { + //small monsters don't eat the contents of the character's inventory + if (Mass < target.AnimController.Mass) + { + target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); + } + //only one limb left, the character is now full eaten Entity.Spawner?.AddToRemoveQueue(target); @@ -442,6 +460,16 @@ namespace Barotrauma //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; + + if (aiming && movement.Length() <= 0.1f) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; + TargetMovement = new Vector2(0.0f, -0.1f); + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } + if (!isMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); @@ -787,7 +815,7 @@ namespace Barotrauma { limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true); } - if (limb.Params.BlinkFrequency > 0) + if (limb.Params.BlinkFrequency > 0 && !limb.Params.OnlyBlinkInWater) { limb.UpdateBlink(deltaTime, MainLimb.Rotation); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 7e04b454f..37a5a7029 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -2,7 +2,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; @@ -73,6 +72,20 @@ namespace Barotrauma set { _humanRunParams = value; } } + private HumanCrouchParams _humanCrouchParams; + public HumanCrouchParams HumanCrouchParams + { + get + { + if (_humanCrouchParams == null) + { + _humanCrouchParams = HumanCrouchParams.GetDefaultAnimParams(character); + } + return _humanCrouchParams; + } + set { _humanCrouchParams = value; } + } + private HumanSwimSlowParams _humanSwimSlowParams; public HumanSwimSlowParams HumanSwimSlowParams { @@ -102,8 +115,11 @@ namespace Barotrauma } public new HumanGroundedParams CurrentGroundedParams => base.CurrentGroundedParams as HumanGroundedParams; + public new HumanSwimParams CurrentSwimParams => base.CurrentSwimParams as HumanSwimParams; + public IHumanAnimation CurrentHumanAnimParams => CurrentAnimationParams as IHumanAnimation; + public override GroundedMovementParams WalkParams { get { return HumanWalkParams; } @@ -130,27 +146,8 @@ namespace Barotrauma public bool Crouching; - private float upperArmLength = 0.0f, forearmLength = 0.0f; - - public float ArmLength => upperArmLength + forearmLength; - - public Vector2 RightHandIKPos - { - get; - private set; - } - public Vector2 LeftHandIKPos - { - get; - private set; - } - - private LimbJoint rightShoulder, leftShoulder; - private float upperLegLength = 0.0f, lowerLegLength = 0.0f; - private bool aiming; - private readonly float movementLerp; private float cprAnimTimer; @@ -161,43 +158,9 @@ namespace Barotrauma //prevents rapid switches between swimming/walking if the water level is fluctuating around the minimum swimming depth private float swimmingStateLockTimer; - private float useItemTimer; - - public override float? TorsoPosition - { - get - { - return Crouching && !swimming ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition; - } - } - - public override float? HeadPosition - { - get - { - return Crouching && !swimming ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition; - } - } - - public override float? TorsoAngle - { - get - { - return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle; - } - } - - public override float? HeadAngle - { - get - { - return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle; - } - } - public float HeadLeanAmount => CurrentGroundedParams.HeadLeanAmount; public float TorsoLeanAmount => CurrentGroundedParams.TorsoLeanAmount; - public Vector2 FootMoveOffset => (Crouching ? CurrentGroundedParams.CrouchingFootMoveOffset : CurrentGroundedParams.FootMoveOffset) * RagdollParams.JointScale; + public Vector2 FootMoveOffset => CurrentGroundedParams.FootMoveOffset * RagdollParams.JointScale; public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale; public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale; @@ -207,7 +170,7 @@ namespace Barotrauma { get { - float shoulderHeight = Collider.height / 2.0f - 0.1f; + float shoulderHeight = Collider.height / 2.0f; if (inWater) { shoulderHeight += 0.4f; @@ -229,61 +192,12 @@ namespace Barotrauma movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f); } - public override void Recreate(RagdollParams ragdollParams) + public override void Recreate(RagdollParams ragdollParams = null) { base.Recreate(ragdollParams); - CalculateArmLengths(); CalculateLegLengths(); } - private void CalculateArmLengths() - { - //calculate arm and forearm length (atm this assumes that both arms are the same size) - Limb rightForearm = GetLimb(LimbType.RightForearm); - Limb rightHand = GetLimb(LimbType.RightHand); - if (rightHand == null) { return; } - - rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm); - leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm); - Vector2 localAnchorShoulder = Vector2.Zero; - Vector2 localAnchorElbow = Vector2.Zero; - if (rightShoulder != null) - { - localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB; - } - LimbJoint rightElbow = rightForearm == null ? - GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) : - GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm); - if (rightElbow != null) - { - localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB; - } - upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow); - if (rightElbow != null) - { - if (rightForearm == null) - { - forearmLength = Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); - } - else - { - LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); - if (rightWrist != null) - { - forearmLength = Vector2.Distance( - rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, - rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - - forearmLength += Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); - } - } - } - } - private void CalculateLegLengths() { //calculate upper and lower leg length (atm this assumes that both legs are the same size) @@ -291,7 +205,7 @@ namespace Barotrauma LimbType lowerLegType = LimbType.RightLeg; LimbType footType = LimbType.RightFoot; - var waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLegType); + var waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLegType) ?? GetJointBetweenLimbs(LimbType.Torso, upperLegType); Vector2 localAnchorWaist = Vector2.Zero; Vector2 localAnchorKnee = Vector2.Zero; if (waistJoint != null) @@ -314,22 +228,17 @@ namespace Barotrauma ankleJoint.LimbA.type == footType ? ankleJoint.LocalAnchorA : ankleJoint.LocalAnchorB, GetLimb(footType).PullJointLocalAnchorA); } - private LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB) - { - return LimbJoints.FirstOrDefault(lj => - (lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) || - (lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB)); - } public override void UpdateAnim(float deltaTime) { if (Frozen) return; if (MainLimb == null) { return; } - levitatingCollider = true; + levitatingCollider = !IsHanging; ColliderIndex = Crouching && !swimming ? 1 : 0; if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false || - (ForceSelectAnimationType != AnimationType.Walk && ForceSelectAnimationType != AnimationType.NotDefined)) + character.SelectedConstruction?.GetComponent() != null || + (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; ColliderIndex = 0; @@ -432,42 +341,25 @@ namespace Barotrauma midPos += Vector2.Transform(new Vector2(-0.3f * Dir, -0.2f), torsoTransform); if (rightHand.PullJointEnabled) midPos = (midPos + rightHand.PullJointWorldAnchorB) / 2.0f; - - HandIK(rightHand, midPos); - HandIK(leftHand, midPos); + HandIK(rightHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); + HandIK(leftHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); } else if (character.AnimController.AnimationTestPose) { - var leftHand = GetLimb(LimbType.LeftHand); - var rightHand = GetLimb(LimbType.RightHand); - var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); - rightHand.Disabled = true; - leftHand.Disabled = true; - Vector2 midPos = waist.SimPosition; - HandIK(rightHand, midPos + new Vector2(-1, -0.2f) * Dir); - HandIK(leftHand, midPos + new Vector2(1, -0.2f) * Dir); - - var leftFoot = GetLimb(LimbType.LeftFoot); - var rightFoot = GetLimb(LimbType.RightFoot); - rightFoot.Disabled = true; - leftFoot.Disabled = true; - // The code here is a bit obscure, but it's pretty much copy-pasted from the block that is used for crouching. - for (int i = -1; i < 2; i += 2) - { - Vector2 footPos = GetColliderBottom(); - footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); - var foot = i == -1 ? rightFoot : leftFoot; - MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true); - } + ApplyTestPose(); } else { - if (Anim != Animation.UsingConstruction) ResetPullJoints(); + if (Anim != Animation.UsingConstruction) + { + ResetPullJoints(); + } } if (SimplePhysicsEnabled) { UpdateStandingSimple(); + IsHanging = false; return; } @@ -531,9 +423,11 @@ namespace Barotrauma { limb.Disabled = false; } - + wasAiming = aiming; aiming = false; - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) return; + wasAimingMelee = aimingMelee; + aimingMelee = false; + IsHanging = false; } void UpdateStanding() @@ -628,7 +522,7 @@ namespace Barotrauma Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); } - getUpForce = getUpForce * Math.Max(head.SimPosition.Y - colliderPos.Y, 0.5f); + getUpForce *= Math.Max(head.SimPosition.Y - colliderPos.Y, 0.5f); torso.PullJointEnabled = true; head.PullJointEnabled = true; @@ -700,9 +594,12 @@ namespace Barotrauma float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, 50.0f); + torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + } + if (HeadAngle.HasValue) + { + head.body.SmoothRotate(HeadAngle.Value * Dir, CurrentGroundedParams.HeadTorque); } - if (HeadAngle.HasValue) head.body.SmoothRotate(HeadAngle.Value * Dir, 50.0f); if (!onGround) { @@ -733,12 +630,23 @@ namespace Barotrauma Vector2 footPos = stepSize * -i; footPos += new Vector2(Math.Sign(movement.X) * FootMoveOffset.X, FootMoveOffset.Y); - if (footPos.Y < 0.0f) footPos.Y = -0.15f; + if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); + if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + { + // Calculate the foot y dynamically based on the foot position relative to the waist, + // so that the foot aims higher when it's behind the waist and lower when it's in the front. + float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; + float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); + footPos.Y *= xFactor; + } + if (onSlope && Stairs == null) { footPos.Y *= 2.0f; @@ -760,7 +668,7 @@ namespace Barotrauma foot.DebugTargetPos = colliderPos + footPos; MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootRotateStrength, CurrentGroundedParams.FootAngleInRadians); + CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); } } @@ -776,58 +684,55 @@ namespace Barotrauma if (rightHand != null && !rightHand.Disabled) { - HandIK(rightHand, torso.SimPosition + posAddition + - new Vector2( - -handPos.X, - (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.HandMoveStrength); + HandIK(rightHand, + torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), + CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); } - if (leftHand != null && !leftHand.Disabled) { - HandIK(leftHand, torso.SimPosition + posAddition + - new Vector2( - handPos.X, - (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.HandMoveStrength); + HandIK(leftHand, + torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), + CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); } - } else { for (int i = -1; i < 2; i += 2) { Vector2 footPos = colliderPos; - if (Crouching) { - footPos = new Vector2( - Math.Sign(stepSize.X * i) * Dir * 0.4f, - colliderPos.Y); + footPos = new Vector2(Math.Sign(stepSize.X * i) * Dir * 0.35f, colliderPos.Y); if (Math.Sign(footPos.X) != Math.Sign(Dir)) { //lift the foot at the back up a bit footPos.Y += 0.15f; } - footPos.X += torso.SimPosition.X; + footPos.X += colliderPos.X; } else { footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); } - if (Stairs == null) { footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y); } - var foot = i == -1 ? rightFoot : leftFoot; - if (foot != null && !foot.Disabled) { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; - MoveLimb(foot, footPos, CurrentGroundedParams.FootMoveStrength); - FootIK(foot, footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootRotateStrength, CurrentGroundedParams.FootAngleInRadians); + float footMoveForce = CurrentGroundedParams.FootMoveStrength; + float legBendTorque = CurrentGroundedParams.LegBendTorque; + if (Crouching) + { + // Keeps the pose + legBendTorque = 100; + footMoveForce *= 2; + } + MoveLimb(foot, footPos, footMoveForce); + FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); } } @@ -843,18 +748,26 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.1f, 0.1f), arm.Mass * 10.0f); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength); } //get the elbow to a neutral rotation if (Math.Abs(hand.body.AngularVelocity) < 10.0f) { - LimbJoint elbow = GetJointBetweenLimbs(armType, hand.type) ?? GetJointBetweenLimbs(armType, foreArmType); + var forearm = GetLimb(foreArmType) ?? hand; + LimbJoint elbow = GetJointBetweenLimbs(armType, foreArmType) ?? GetJointBetweenLimbs(armType, hand.type); if (elbow != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f); + float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength); } } + // Try to keep the wrist straight + LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type); + if (wrist != null) + { + hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength); + } } } } @@ -904,8 +817,8 @@ namespace Barotrauma { foreach (Gap gap in currentHull.ConnectedGaps) { - if (gap.IsHorizontal || gap.Open <= 0.0f) continue; - if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) continue; + if (gap.IsHorizontal || gap.Open <= 0.0f) { continue; } + if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > currentHull.Position.Y) @@ -925,7 +838,7 @@ namespace Barotrauma } } - surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 0.4f) - surfacePos; + surfaceLimiter = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f) - surfacePos; surfaceLimiter = Math.Max(1.0f, surfaceLimiter); if (surfaceLimiter > 50.0f) { return; } } @@ -938,25 +851,29 @@ namespace Barotrauma float rotation = MathHelper.WrapAngle(Collider.Rotation); rotation = MathHelper.ToDegrees(rotation); - if (rotation < 0.0f) rotation += 360; - + if (rotation < 0.0f) + { + rotation += 360; + } if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) { if (rotation > 20 && rotation < 170) + { TargetDir = Direction.Left; + } else if (rotation > 190 && rotation < 340) + { TargetDir = Direction.Right; + } } - float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f) { if (!aiming) { float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } } else @@ -965,11 +882,9 @@ namespace Barotrauma { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } } @@ -979,19 +894,19 @@ namespace Barotrauma if (TorsoAngle.HasValue) { - torso.body.SmoothRotate(Collider.Rotation + TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque); + torso.body.SmoothRotate(Collider.Rotation + TorsoAngle.Value * Dir, CurrentSwimParams.TorsoTorque); } else { - torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.SteerTorque); + torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } if (HeadAngle.HasValue) { - head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque); + head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.SteerTorque); + head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); } //dont try to move upwards if head is already out of water @@ -1014,7 +929,7 @@ namespace Barotrauma head.body.ApplyTorque(Dir); } - movement.Y = movement.Y - (surfaceLimiter - 1.0f) * 0.01f; + movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / 50.0f)); } bool isNotRemote = true; @@ -1032,25 +947,25 @@ namespace Barotrauma float legMoveMultiplier = 1.0f; if (movement.LengthSquared() < 0.001f) { - //TODO: expose these? + // Swimming in place (TODO: expose?) legMoveMultiplier = 0.3f; legCyclePos += 0.4f; handCyclePos += 0.1f; } - var waist = GetLimb(LimbType.Waist); + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength); Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * legMoveMultiplier, 0.0f); transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation)); - float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); + float legTorque = CurrentSwimParams.LegTorque * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); if (rightFoot != null && !rightFoot.Disabled) { - FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); + FootIK(rightFoot, footPos - transformedFootPos, legTorque, CurrentSwimParams.FootTorque, CurrentSwimParams.FootAngleInRadians); } if (leftFoot != null && !leftFoot.Disabled) { - FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); + FootIK(leftFoot, footPos + transformedFootPos, legTorque, CurrentSwimParams.FootTorque, CurrentSwimParams.FootAngleInRadians); } handPos = (torso.SimPosition + head.SimPosition) / 2.0f; @@ -1059,7 +974,7 @@ namespace Barotrauma // -> hands just float around if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f) { - handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation); + handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.2f, torso.Rotation); float wobbleAmount = 0.1f; @@ -1067,20 +982,20 @@ namespace Barotrauma { MoveLimb(rightHand, new Vector2( handPos.X + (float)Math.Sin(handCyclePos / 1.5f) * wobbleAmount, - handPos.Y + (float)Math.Sin(handCyclePos / 3.5f) * wobbleAmount - 0.25f), 1.5f); + handPos.Y + (float)Math.Sin(handCyclePos / 3.5f) * wobbleAmount - 0.25f), CurrentSwimParams.ArmMoveStrength); } if (leftHand != null && !leftHand.Disabled) { MoveLimb(leftHand, new Vector2( handPos.X + (float)Math.Sin(handCyclePos / 2.0f) * wobbleAmount, - handPos.Y + (float)Math.Sin(handCyclePos / 3.0f) * wobbleAmount - 0.25f), 1.5f); + handPos.Y + (float)Math.Sin(handCyclePos / 3.0f) * wobbleAmount - 0.25f), CurrentSwimParams.ArmMoveStrength); } return; } - handPos += head.LinearVelocity * 0.1f; + handPos += head.LinearVelocity.ClampLength(1.0f) * 0.1f; // Not sure why the params has to be flipped, but it works. var handMoveAmount = CurrentSwimParams.HandMoveAmount.Flip(); @@ -1095,8 +1010,18 @@ namespace Barotrauma Vector2 rightHandPos = new Vector2(-handPosX, -handPosY) + handMoveOffset; rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); - - HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty())); + float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()), 1.0f); + if (character.Inventory != null && character.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) + { + speedMultiplier = Math.Min(speedMultiplier, 0.1f); + } + HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); + // Try to keep the wrist straight + LimbJoint wrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); + if (wrist != null) + { + rightHand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * rightHand.Mass * 100f * CurrentSwimParams.HandMoveStrength); + } } if (leftHand != null && !leftHand.Disabled) @@ -1104,8 +1029,18 @@ namespace Barotrauma Vector2 leftHandPos = new Vector2(handPosX, handPosY) + handMoveOffset; leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X); leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix); - - HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty())); + float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()), 1.0f); + if (character.Inventory != null && character.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) + { + speedMultiplier = Math.Min(speedMultiplier, 0.1f); + } + HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); + // Try to keep the wrist straight + LimbJoint wrist = GetJointBetweenLimbs(LimbType.LeftForearm, LimbType.LeftHand); + if (wrist != null) + { + leftHand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * leftHand.Mass * 100f * CurrentSwimParams.HandMoveStrength); + } } } @@ -1161,15 +1096,16 @@ namespace Barotrauma } float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; + float headPos = HeadPosition ?? 0; + float torsoPos = TorsoPosition ?? 0; + MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f); + MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); - MoveLimb(head, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f); - MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f); - - Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); + Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); Vector2 handPos = new Vector2( ladderSimPos.X, - bottomPos + WalkParams.TorsoPosition + movement.Y * 0.1f - ladderSimPos.Y); + bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); //prevent the hands from going above the top of the ladders handPos.Y = Math.Min(-0.5f, handPos.Y); @@ -1245,8 +1181,9 @@ namespace Barotrauma if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } //apply forces to the collider to move the Character up/down - Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - head.body.SmoothRotate(0.0f); + Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); + float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; + head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); if (!character.SelectedConstruction.Prefab.Triggers.Any()) { @@ -1385,6 +1322,8 @@ namespace Barotrauma { target.Oxygen += deltaTime * 0.5f; //Stabilize them } + + bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR); int skill = (int)character.GetSkillLevel("medical"); //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) @@ -1401,13 +1340,19 @@ namespace Barotrauma { if (target.Oxygen < -10.0f) { - //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); - character.Oxygen -= (1.0f / stabilizationAmount) * deltaTime; //Worse skill = more oxygen required - if (character.Oxygen > 0.0f) target.Oxygen += stabilizationAmount * deltaTime; //we didn't suffocate yet did we - - //DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua); + if (powerfulCPR) + { + //prevent the patient from suffocating no matter how fast their oxygen level is dropping + target.Oxygen = Math.Max(target.Oxygen, -10.0f); + } + else + { + //stabilize the oxygen level but don't allow it to go positive and revive the character yet + float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); + character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required + if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we + } } } } @@ -1442,6 +1387,8 @@ namespace Barotrauma reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent); reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax); + if (powerfulCPR) { reviveChance *= 2.0f; } + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance) { //increase oxygen and clamp it above zero @@ -1463,7 +1410,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.Position + Vector2.UnitY * 150.0f); + character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER @@ -1622,7 +1569,10 @@ namespace Barotrauma { Vector2 pullLimbAnchor = targetLimb.SimPosition; pullLimb.PullJointMaxForce = 5000.0f; - targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); + if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) + { + targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f); + } Vector2 shoulderPos = rightShoulder.WorldAnchorA; Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY; @@ -1679,7 +1629,7 @@ namespace Barotrauma } //limit movement if moving away from the target - if (Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) + if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging) && Vector2.Dot(target.WorldPosition - WorldPosition, targetMovement) < 0) { targetMovement *= MathHelper.Clamp(1.5f - dist, 0.0f, 1.0f); } @@ -1698,224 +1648,6 @@ namespace Barotrauma } } - public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) - { - for (int i = 0; i < 2; i++) - { - Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand); - - pullLimb.Disabled = true; - - pullLimb.PullJointEnabled = true; - pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos; - pullLimb.PullJointMaxForce = 500.0f; - } - } - - //TODO: refactor this method, it's way too convoluted - public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) - { - if (character.Stun > 0.0f || character.IsIncapacitated) - { - aim = false; - } - - //calculate the handle positions - Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - // TODO: don't create new arrays, reuse - Vector2[] transformedHandlePos = new Vector2[2]; - transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); - - Limb head = GetLimb(LimbType.Head); - Limb torso = GetLimb(LimbType.Torso); - Limb leftHand = GetLimb(LimbType.LeftHand); - Limb rightHand = GetLimb(LimbType.RightHand); - - // TODO: Remove this. Provide the position in params. - Vector2 itemPos = aim ? aimPos : holdPos; - - var controller = character.SelectedConstruction?.GetComponent(); - bool usingController = controller != null && !controller.AllowAiming; - bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; - - float itemAngle; - - Holdable holdable = item.GetComponent(); - - if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - - Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; - - holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torso.body.Rotation * Dir; - - itemAngle = (torso.body.Rotation + holdAngle * Dir); - - if (holdable.ControlPose) - { - head?.body.SmoothRotate(itemAngle); - - if (TargetMovement == Vector2.Zero && inWater) - { - torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; - torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); - } - - aiming = true; - } - - } - else - { - itemAngle = torso.body.Rotation + holdAngle * Dir; - } - - Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; - if (itemPos == Vector2.Zero || isClimbing || usingController) - { - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) - { - if (rightHand == null || rightHand.IsSevered) { return; } - transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; - itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); - } - else if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) - { - if (leftHand == null || leftHand.IsSevered) { return; } - transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; - itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); - } - } - else - { - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) - { - if (rightHand == null || rightHand.IsSevered) { return; } - transformedHoldPos = rightShoulder.WorldAnchorA; - rightHand.Disabled = true; - } - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) - { - if (leftHand == null || leftHand.IsSevered) { return; } - transformedHoldPos = leftShoulder.WorldAnchorA; - leftHand.Disabled = true; - } - - itemPos.X *= Dir; - transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle)); - } - - item.body.ResetDynamics(); - - Vector2 currItemPos = (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) ? - rightHand.PullJointWorldAnchorA - transformedHandlePos[0] : - leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; - - if (!MathUtils.IsValid(currItemPos)) - { - string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " + - currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA + - ", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] + - ", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] + - ", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle + - ", collider pos: " + character.SimPosition; - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg); - - return; - } - - if (holdable.Pusher != null) - { - if (character.Stun > 0.0f || character.IsIncapacitated) - { - holdable.Pusher.Enabled = false; - } - else - { - if (!holdable.Pusher.Enabled) - { - holdable.Pusher.Enabled = true; - holdable.Pusher.ResetDynamics(); - holdable.Pusher.SetTransform(currItemPos, itemAngle); - } - else - { - holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = holdAngle * Dir; - - holdable.Pusher.MoveToTargetPosition(true); - - currItemPos = holdable.Pusher.SimPosition; - itemAngle = holdable.Pusher.Rotation; - } - } - } - - item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); - - if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero) - { - for (int i = 0; i < 2; i++) - { - if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; } - HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i]); - } - } - } - - private void HandIK(Limb hand, Vector2 pos, float force = 1.0f) - { - Vector2 shoulderPos; - - Limb arm, forearm; - if (hand.type == LimbType.LeftHand) - { - if (leftShoulder == null) { return; } - shoulderPos = leftShoulder.WorldAnchorA; - arm = GetLimb(LimbType.LeftArm); - forearm = GetLimb(LimbType.LeftForearm); - LeftHandIKPos = pos; - } - else - { - if (rightShoulder == null) { return; } - shoulderPos = rightShoulder.WorldAnchorA; - arm = GetLimb(LimbType.RightArm); - forearm = GetLimb(LimbType.RightForearm); - RightHandIKPos = pos; - } - if (arm == null) { return; } - - //distance from shoulder to holdpos - float c = Vector2.Distance(pos, shoulderPos); - c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f); - - float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + MathHelper.PiOver2; - - float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir; - float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir; - - //make sure the arm angle "has the same number of revolutions" as the arm - while (arm.Rotation - armAngle > MathHelper.Pi) - { - armAngle += MathHelper.TwoPi; - } - while (arm.Rotation - armAngle < -MathHelper.Pi) - { - armAngle -= MathHelper.TwoPi; - } - - arm?.body.SmoothRotate((armAngle - upperArmAngle), 20.0f * force * arm.Mass, wrapAngle: false); - forearm?.body.SmoothRotate((armAngle + lowerArmAngle), 20.0f * force * forearm.Mass, wrapAngle: false); - hand?.body.SmoothRotate((armAngle + lowerArmAngle), 100.0f * force * hand.Mass, wrapAngle: false); - } - private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) @@ -1939,12 +1671,12 @@ namespace Barotrauma upperLeg = GetLimb(LimbType.RightThigh); lowerLeg = GetLimb(LimbType.RightLeg); } - var torso = GetLimb(LimbType.Torso); - var waist = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type); + Limb torso = GetLimb(LimbType.Torso); + LimbJoint waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type) ?? GetJointBetweenLimbs(LimbType.Torso, upperLeg.type); Vector2 waistPos = Vector2.Zero; - if (waist != null) + if (waistJoint != null) { - waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB; + waistPos = waistJoint.LimbA == upperLeg ? waistJoint.WorldAnchorA : waistJoint.WorldAnchorB; } //distance from waist joint to the target position @@ -1982,47 +1714,6 @@ namespace Barotrauma foot.body.SmoothRotate((legAngle - (lowerLegAngle + footAngle) * Dir), foot.Mass * footTorque, wrapAngle: false); } - public override void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) - { - useItemTimer = 0.5f; - Anim = Animation.UsingConstruction; - - if (!allowMovement) - { - TargetMovement = Vector2.Zero; - TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; - float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); - if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) - { - TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); - } - } - - if (!character.Enabled) { return; } - - Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos); - if (character.Submarine != null) - { - handSimPos -= character.Submarine.SimPosition; - } - - var leftHand = GetLimb(LimbType.LeftHand); - if (leftHand != null) - { - leftHand.Disabled = true; - leftHand.PullJointEnabled = true; - leftHand.PullJointWorldAnchorB = handSimPos; - } - - var rightHand = GetLimb(LimbType.RightHand); - if (rightHand != null) - { - rightHand.Disabled = true; - rightHand.PullJointEnabled = true; - rightHand.PullJointWorldAnchorB = handSimPos; - } - } - public override void Flip() { base.Flip(); @@ -2041,7 +1732,8 @@ namespace Barotrauma { heldItem.FlipX(relativeToSub: false); } - heldItem.FlipX(relativeToSub: false); + // TODO: was this added by a mistake? + //heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) @@ -2100,5 +1792,18 @@ namespace Barotrauma } } + public override float GetSpeed(AnimationType type) + { + if (type == AnimationType.Crouch) + { + if (!CanWalk) + { + DebugConsole.ThrowError($"{character.SpeciesName} cannot crouch!"); + return 0; + } + return IsMovingBackwards ? HumanCrouchParams.MovementSpeed * HumanCrouchParams.BackwardsMovementMultiplier : HumanCrouchParams.MovementSpeed; + } + return base.GetSpeed(type); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 158101565..5184eaad5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -126,7 +126,8 @@ namespace Barotrauma protected float surfaceY; protected bool inWater, headInWater; - public bool onGround; + protected bool onGround; + public bool OnGround => onGround; private Vector2 lastFloorCheckPos; private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms; @@ -396,12 +397,18 @@ namespace Barotrauma if (character.IsHusk && character.Params.UseHuskAppendage) { + bool inEditor = false; +#if CLIENT + inEditor = Screen.Selected == GameMain.CharacterEditorScreen; +#endif + var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); if (characterPrefab?.XDocument != null) { var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { + if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this); } } @@ -889,7 +896,7 @@ namespace Barotrauma /// if false, force is applied to the position of pullJoint - protected void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false) + public void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false) { limb.MoveToPos(pos, amount, pullFromCenter); } @@ -976,8 +983,7 @@ namespace Barotrauma 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); + Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); } } @@ -1101,10 +1107,11 @@ namespace Barotrauma } public bool forceStanding; + public bool forceNotStanding; public void Update(float deltaTime, Camera cam) { - if (!character.Enabled || Frozen || Invalid) { return; } + if (!character.Enabled || character.Removed || Frozen || Invalid || Collider == null || Collider.Removed) { return; } while (impactQueue.Count > 0) { @@ -1213,24 +1220,24 @@ namespace Barotrauma //the room where the ragdoll is in is used as the "guess", meaning that it's checked first Hull limbHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull); - bool prevInWater = limb.inWater; - limb.inWater = false; + bool prevInWater = limb.InWater; + limb.InWater = false; if (forceStanding) { - limb.inWater = false; + limb.InWater = false; } else if (limbHull == null) { //limb isn't in any room -> it's in the water - limb.inWater = true; + limb.InWater = true; if (limb.type == LimbType.Head) headInWater = true; } else if (limbHull.WaterVolume > 0.0f && Submarine.RectContains(limbHull.Rect, limb.Position)) { if (limb.Position.Y < limbHull.Surface) { - limb.inWater = true; + limb.InWater = true; surfaceY = limbHull.Surface; if (limb.type == LimbType.Head) { @@ -1238,7 +1245,7 @@ namespace Barotrauma } } //the limb has gone through the surface of the water - if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.inWater != prevInWater) + if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater) { Splash(limb, limbHull); @@ -1272,6 +1279,7 @@ namespace Barotrauma } } UpdateProjSpecific(deltaTime, cam); + forceNotStanding = false; } private void CheckBodyInRest(float deltaTime) @@ -1348,19 +1356,19 @@ namespace Barotrauma string errorMsg = null; if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f) { - errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll."; + errorMsg = GetBodyName() + " position invalid (" + body.SimPosition + ", character: " + character.Name + ")."; } else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f) { - errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + "), resetting the ragdoll."; + errorMsg = GetBodyName() + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + ")."; } else if (!MathUtils.IsValid(body.Rotation)) { - errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: " + character.Name + "), resetting the ragdoll."; + errorMsg = GetBodyName() + " rotation invalid (" + body.Rotation + ", character: " + character.Name + ")."; } else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f) { - errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + "), resetting the ragdoll."; + errorMsg = GetBodyName() + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + ")."; } if (errorMsg != null) { @@ -1432,9 +1440,10 @@ namespace Barotrauma //throwing conscious/moving characters around takes more force -> double the flow force if (character.CanMove) { flowForce *= 2.0f; } + flowForce *= 1 - Math.Clamp(character.GetStatValue(StatTypes.FlowResistance), 0f, 1f); float flowForceMagnitude = flowForce.Length(); - float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length; + float limbMultipier = limbs.Count(l => l.InWater) / (float)limbs.Length; //if the force strong enough, stun the character to let it get thrown around by the water if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold) { @@ -1468,11 +1477,11 @@ namespace Barotrauma if (flowForce.LengthSquared() > 0.001f) { - Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + Collider.ApplyForce(flowForce); foreach (Limb limb in limbs) { - if (!limb.inWater) { continue; } - limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + if (!limb.InWater) { continue; } + limb.body.ApplyForce(flowForce); } } } @@ -1505,7 +1514,6 @@ namespace Barotrauma if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); } Vector2 rayEnd = rayStart - new Vector2(0.0f, height); - Vector2 onGroundRayEnd = rayStart - Vector2.UnitY * (Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f); Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom()); Fixture standOnFloorFixture = null; @@ -1570,7 +1578,7 @@ namespace Barotrauma return closestFraction; }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel); - if (standOnFloorFixture != null) + if (standOnFloorFixture != null && !IsHanging) { standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) @@ -1586,7 +1594,25 @@ namespace Barotrauma if (closestFraction == 1) //raycast didn't hit anything { floorNormal = Vector2.UnitY; - return (currentHull == null) ? -1000.0f : ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height); + if (CurrentHull == null) + { + return -1000.0f; + } + else + { + float hullBottom = currentHull.Rect.Y - currentHull.Rect.Height; + //check if there's a connected hull below + foreach (var gap in currentHull.ConnectedGaps) + { + if (!gap.IsRoomToRoom || gap.Open < 1.0f || gap.ConnectedDoor != null || gap.IsHorizontal) { continue; } + if (WorldPosition.X > gap.WorldRect.X && WorldPosition.X < gap.WorldRect.Right && gap.WorldPosition.Y < WorldPosition.Y) + { + var lowerHull = gap.linkedTo[0] == currentHull ? gap.linkedTo[1] : gap.linkedTo[0]; + hullBottom = Math.Min(hullBottom, lowerHull.Rect.Y - lowerHull.Rect.Height); + } + } + return ConvertUnits.ToSimUnits(hullBottom); + } } else { @@ -1607,6 +1633,13 @@ namespace Barotrauma } if (MainLimb == null) { return; } + if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) + { + enemyAI.LatchOntoAI.DeattachFromBody(reset: true); + } + Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true)); + Character.Latchers.Clear(); + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { @@ -1630,6 +1663,16 @@ namespace Barotrauma } } + public bool IsHanging { get; protected set; } + + public void Hang() + { + ResetPullJoints(); + onGround = false; + levitatingCollider = false; + IsHanging = true; + } + protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; @@ -1822,9 +1865,23 @@ namespace Barotrauma public void ReleaseStuckLimbs() { - Limbs.ForEach(l => l.Release()); + // Commented out, because stuck limbs is not a feature that we currently use, as it would require that we sync all the limbs, which we don't do. + //Limbs.ForEach(l => l.Release()); } + public void HideAndDisable(LimbType limbType, float duration = 0, bool ignoreCollisions = true) + { + foreach (var limb in Limbs) + { + if (limb.type == limbType) + { + limb.HideAndDisable(duration, ignoreCollisions); + } + } + } + + public void RestoreTemporarilyDisabled() => Limbs.ForEach(l => l.ReEnable()); + public void Remove() { if (Limbs != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index c89b02b76..e07f73f0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -8,7 +8,8 @@ namespace Barotrauma public enum HitDetection { Distance, - Contact + Contact, + None } public enum AttackContext @@ -271,6 +272,9 @@ namespace Barotrauma statusEffect.SetUser(user); } } + + // used for talents/ability conditions + public Item SourceItem { get; } public List GetMultipliedAfflictions(float multiplier) { @@ -320,6 +324,10 @@ namespace Barotrauma Penetration = Penetration; } + public Attack(XElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) + { + SourceItem = sourceItem; + } public Attack(XElement element, string parentDebugName) { Deserialize(element); @@ -445,7 +453,7 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = target.AddDamage(attacker, worldPosition, this, deltaTime, playSound); + var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; if (targetCharacter != null && targetCharacter.IsDead) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3636f4830..a3975aa27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using Barotrauma.Extensions; +using Barotrauma.Abilities; #if SERVER using System.Text; #endif @@ -128,6 +129,8 @@ namespace Barotrauma } } + public readonly HashSet Latchers = new HashSet(); + protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; @@ -250,6 +253,8 @@ namespace Barotrauma private readonly List lastAttackers = new List(); public IEnumerable LastAttackers => lastAttackers; public Character LastAttacker => lastAttackers.LastOrDefault()?.Character; + public Character LastOrderedCharacter { get; private set; } + public Character SecondLastOrderedCharacter { get; private set; } public Entity LastDamageSource; @@ -261,6 +266,7 @@ namespace Barotrauma public readonly CharacterParams Params; public string SpeciesName => Params.SpeciesName; + public string Group => Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; @@ -306,7 +312,14 @@ namespace Barotrauma public string TraitorCurrentObjective = ""; public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); + + /// + /// Can be used by status effects to check the character's gender + /// public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; + /// + /// Can be used by status effects to check the character's gender + /// public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female; private float attackCoolDown; @@ -461,10 +474,7 @@ namespace Barotrauma } } - public bool AllowInput - { - get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; } - } + public bool AllowInput => !Removed && !IsIncapacitated && Stun <= 0.0f; public bool CanMove { @@ -475,11 +485,10 @@ namespace Barotrauma return true; } } + public bool CanInteract => AllowInput && Params.CanInteract && !LockHands; - public bool CanInteract - { - get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; } - } + // Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction. + public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null; public Vector2 CursorPosition { @@ -581,6 +590,14 @@ namespace Barotrauma } } + /// + /// Can be used by status effects to check whether the characters is in a high-pressure environment + /// + public bool InPressure + { + get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; } + } + public const float KnockbackCooldown = 5.0f; public float KnockbackCooldownTimer; @@ -594,6 +611,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } + if (IsDead) { return true; } return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); } } @@ -628,7 +646,7 @@ namespace Barotrauma public float Stun { - get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; } + get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; } set { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -640,22 +658,14 @@ namespace Barotrauma public bool DisableHealthWindow; - public float Vitality - { - get { return CharacterHealth.Vitality; } - } - - public float Health - { - get { return CharacterHealth.Vitality; } - } - + // These properties needs to be exposed for status effects + public float Vitality => CharacterHealth.Vitality; + public float Health => Vitality; public float HealthPercentage => CharacterHealth.HealthPercentage; - - public float MaxVitality - { - get { return CharacterHealth.MaxVitality; } - } + public float MaxVitality => CharacterHealth.MaxVitality; + public float MaxHealth => MaxVitality; + public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; + public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached; public float Bloodloss { @@ -773,8 +783,6 @@ namespace Barotrauma } } - public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe; - public bool EnableDespawn { get; set; } = true; public CauseOfDeath CauseOfDeath @@ -1138,7 +1146,7 @@ namespace Barotrauma { nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction); } - if (ragdollParams == null) + if (ragdollParams == null && prefab.VariantOf == null) { string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; @@ -1180,6 +1188,7 @@ namespace Barotrauma { LoadHeadAttachments(); } + ApplyStatusEffects(ActionType.OnSpawn, 1.0f); } partial void InitProjSpecific(XElement mainElement); @@ -1375,13 +1384,18 @@ namespace Barotrauma } } } - private List wearableItems = new List(); public float GetSkillLevel(string skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + // apply multipliers first so that multipliers only affect base skill value + foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) + { + skillLevel *= affliction.GetSkillMultiplier(); + } + if (skillIdentifier != null) { for (int i = 0; i < Inventory.Capacity; i++) @@ -1396,10 +1410,8 @@ namespace Barotrauma } } - foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) - { - skillLevel *= affliction.GetSkillMultiplier(); - } + skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); + return skillLevel; } @@ -1436,9 +1448,9 @@ namespace Barotrauma // - dragging someone // - crouching // - moving backwards - public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) && + public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged || HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging)) && (!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) && - !AnimController.IsMovingBackwards; + !AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk); public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed) { @@ -1599,7 +1611,7 @@ namespace Barotrauma return Math.Clamp(reduction, 0, 1f); } - private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f) + private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f) { if (limb != null) { @@ -1632,7 +1644,7 @@ namespace Barotrauma float max; if (AnimController is HumanoidAnimController) { - max = AnimController.InWater ? 0.5f : 0.7f; + max = AnimController.InWater ? 0.5f : 0.8f; } else { @@ -1668,13 +1680,13 @@ namespace Barotrauma AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; } - if (AnimController is HumanoidAnimController) + if (AnimController is HumanoidAnimController humanAnimController) { - ((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch); + humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch); } if (!aiControlled && - AnimController.onGround && + AnimController.OnGround && !AnimController.InWater && AnimController.Anim != AnimController.Animation.UsingConstruction && AnimController.Anim != AnimController.Animation.CPR && @@ -1738,8 +1750,12 @@ namespace Barotrauma } else if (IsKeyDown(InputType.Attack)) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this) { + if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false) + { + currentAttackTarget = default(AttackTargetData); + } currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); } else if (IsPlayer) @@ -2048,7 +2064,7 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null) + public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2063,7 +2079,7 @@ namespace Barotrauma } var item = Inventory.GetItemAt(i); if (item == null) { continue; } - if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } + if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } } return null; } @@ -2080,11 +2096,10 @@ namespace Barotrauma return SelectedCharacter == owner && owner.CanInventoryBeAccessed; } - if (inventory.Owner is Item) + if (inventory.Owner is Item item) { - var owner = (Item)inventory.Owner; - if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } - ItemContainer container = owner.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); + if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } + ItemContainer container = item.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; } } return true; @@ -2223,6 +2238,12 @@ namespace Barotrauma } } + if (SelectedConstruction?.GetComponent()?.TargetItem == item || + HeldItems.Any(it => it.GetComponent()?.TargetItem == item)) + { + return true; + } + if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; } Pickable pickableComponent = item.GetComponent(); @@ -2369,9 +2390,9 @@ namespace Barotrauma { if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this)) { - if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) + if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)) { - FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null; + FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) @@ -2401,7 +2422,7 @@ namespace Barotrauma var head = AnimController.GetLimb(LimbType.Head); bool headInWater = head == null ? AnimController.InWater : - head.inWater; + head.InWater; //climb ladders automatically when pressing up/down inside their trigger area Ladder currentLadder = SelectedConstruction?.GetComponent(); if ((SelectedConstruction == null || currentLadder != null) && @@ -2449,7 +2470,7 @@ namespace Barotrauma { DeselectCharacter(); } - else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract) + else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat)) { SelectCharacter(FocusedCharacter); } @@ -2628,6 +2649,11 @@ namespace Barotrauma UpdateAttackers(deltaTime); + foreach (var characterTalent in characterTalents) + { + characterTalent.UpdateTalent(deltaTime); + } + if (IsDead) { return; } if (GameMain.NetworkMember != null) @@ -2692,6 +2718,11 @@ namespace Barotrauma ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (aiTarget != null) + { + aiTarget.InDetectable = false; + } + UpdateControlled(deltaTime, cam); //Health effects @@ -2715,14 +2746,18 @@ namespace Barotrauma //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; - bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 5.0f * 5.0f; + bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f; + bool wasRagdolled = false; + bool selfRagdolled = false; + if (IsForceRagdolled) { IsRagdolled = IsForceRagdolled; } else if (this != Controlled) { - IsRagdolled = IsKeyDown(InputType.Ragdoll); + wasRagdolled = IsRagdolled; + IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); } //Keep us ragdolled if we were forced or we're too speedy to unragdoll else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll)) @@ -2734,20 +2769,30 @@ namespace Barotrauma } else { - bool wasRagdolled = IsRagdolled; - IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves - if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; } + wasRagdolled = IsRagdolled; + IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves + if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; } } } + if (!wasRagdolled && IsRagdolled) + { + if (selfRagdolled) + { + CheckTalents(AbilityEffectType.OnSelfRagdoll); + } + // currently does not work when you are stunned, like it should + CheckTalents(AbilityEffectType.OnRagdoll); + } + lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); //ragdoll button if (IsRagdolled || !CanMove) { - if (AnimController is HumanoidAnimController) + if (AnimController is HumanoidAnimController humanAnimController) { - ((HumanoidAnimController)AnimController).Crouching = false; + humanAnimController.Crouching = false; } AnimController.ResetPullJoints(); SelectedConstruction = null; @@ -2825,7 +2870,7 @@ namespace Barotrauma // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) reduction *= 0.5f; } - enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction); + enemy.Damage = Math.Max(0.0f, enemy.Damage - reduction); } } } @@ -3065,30 +3110,47 @@ namespace Barotrauma //set the character order only if the character is close enough to hear the message if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } - if (order.OrderGiver != orderGiver) + if (order != null && order.OrderGiver != orderGiver) { order.OrderGiver = orderGiver; } - // If there's another character operating the same device, make them dismiss themself - if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null) + switch (order?.Category) { - foreach (var character in CharacterList) - { - if (character == this) { continue; } - if (character.TeamID != TeamID) { continue; } - if (!(character.AIController is HumanAIController)) { continue; } - if (!HumanAIController.IsActive(character)) { continue; } - foreach (var currentOrder in character.CurrentOrders) + case OrderCategory.Operate when order?.TargetEntity != null: + // If there's another character operating the same device, make them dismiss themself + foreach (var character in CharacterList) + { + if (character == this) { continue; } + if (character.TeamID != TeamID) { continue; } + if (!(character.AIController is HumanAIController)) { continue; } + if (!HumanAIController.IsActive(character)) { continue; } + foreach (var currentOrder in character.CurrentOrders) + { + if (currentOrder.Order == null) { continue; } + if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Order.Identifier != order.Identifier) { continue; } + if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } + character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + break; + } + } + break; + case OrderCategory.Movement: + // If there character has another movement order, dismiss that order + OrderInfo? orderToReplace = null; + foreach (var currentOrder in CurrentOrders) { if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } - if (currentOrder.Order.Identifier != order.Identifier) { continue; } - if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } - character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + if (currentOrder.Order.Category != OrderCategory.Movement) { continue; } + orderToReplace = currentOrder; break; } - } + if (orderToReplace.HasValue) + { + SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force); + } + break; } // Prevent adding duplicate orders @@ -3096,6 +3158,19 @@ namespace Barotrauma OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); AddCurrentOrder(newOrderInfo); + + if (orderGiver != null) + { + var abilityOrderedCharacter = new AbilityCharacter(this); + orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); + + if (orderGiver.LastOrderedCharacter != this) + { + orderGiver.SecondLastOrderedCharacter = orderGiver.LastOrderedCharacter; + orderGiver.LastOrderedCharacter = this; + } + } + if (AIController is HumanAIController humanAI) { humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); @@ -3341,9 +3416,40 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; + AbilityAttackData attackData = new AbilityAttackData(attack, this); + if (attacker != null) + { + attackData.Attacker = attacker; + attacker.CheckTalents(AbilityEffectType.OnAttack, attackData); + CheckTalents(AbilityEffectType.OnAttacked, attackData); + attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier); + if (attacker.TeamID == TeamID) + { + attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier); + } + } + + IEnumerable attackAfflictions; + + if (attackData.Afflictions != null) + { + attackAfflictions = attackData.Afflictions.Union(attack.Afflictions.Keys); + } + else + { + attackAfflictions = attack.Afflictions.Keys; + } + var attackResult = targetLimb == null ? - AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) : - DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration); + AddDamage(worldPosition, attackAfflictions, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier * attackData.DamageMultiplier) : + DamageLimb(worldPosition, targetLimb, attackAfflictions, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier * attackData.DamageMultiplier, penetration: penetration + attackData.AddedPenetration); + + if (attacker != null) + { + var abilityAttackResult = new AbilityAttackResult(attackResult); + attacker.CheckTalents(AbilityEffectType.OnAttackResult, abilityAttackResult); + CheckTalents(AbilityEffectType.OnAttackedResult, abilityAttackResult); + } if (limbHit == null) { return new AttackResult(); } Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; @@ -3429,9 +3535,9 @@ namespace Barotrauma } } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f) { - return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker); + return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier); } public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) @@ -3440,11 +3546,6 @@ namespace Barotrauma if (Removed) { return new AttackResult(); } - if (attacker != null && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) - { - if (attacker.TeamID == TeamID) { return new AttackResult(); } - } - float closestDistance = 0.0f; foreach (Limb limb in AnimController.Limbs) { @@ -3461,6 +3562,13 @@ namespace Barotrauma public void RecordKill(Character target) { + var abilityCharacter = new AbilityCharacter(target); + foreach (Character attackerCrewmember in GetFriendlyCrew(this)) + { + attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, abilityCharacter); + } + CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacter); + if (!IsOnPlayerTeam) { return; } if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } GameMain.Config.KilledCreatures.Add(target.SpeciesName); @@ -3500,7 +3608,11 @@ namespace Barotrauma if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { - if (attacker.TeamID == TeamID) { return new AttackResult(); } + if (attacker.TeamID == TeamID) + { + afflictions = afflictions.Where(a => !a.Prefab.IsBuff); + if (!afflictions.Any()) { return new AttackResult(); } + } } #if CLIENT @@ -3528,7 +3640,7 @@ namespace Barotrauma bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); float prevVitality = CharacterHealth.Vitality; - AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration); + AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker); CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking); if (attacker != this) { @@ -3555,6 +3667,7 @@ namespace Barotrauma ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } + return attackResult; } @@ -3571,16 +3684,14 @@ namespace Barotrauma { float attackerSkillLevel = attacker.GetSkillLevel("weapons"); attacker.Info?.IncreaseSkillLevel("weapons", - -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f), - attacker.Position + Vector2.UnitY * 100.0f); + -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); } } else if (healthChange > 0.0f) { float attackerSkillLevel = attacker.GetSkillLevel("medical"); attacker.Info?.IncreaseSkillLevel("medical", - healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f), - attacker.Position + Vector2.UnitY * 100.0f); + healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f)); } } @@ -3594,6 +3705,7 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } + if (newStun > 0 && Params.Health.StunImmunity) { return; } if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } if (Math.Sign(newStun) != Math.Sign(Stun)) { @@ -3615,10 +3727,7 @@ namespace Barotrauma if (statusEffect.type != actionType) { continue; } if (statusEffect.type == ActionType.OnDamaged) { - if (statusEffect.AllowedAfflictions != null && (LastDamage.Afflictions == null || LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) - { - continue; - } + if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } if (statusEffect.OnlyPlayerTriggered) { if (LastAttacker == null || !LastAttacker.IsPlayer) @@ -3681,7 +3790,7 @@ namespace Barotrauma } } - private void Implode(bool isNetworkMessage = false) + public void Implode(bool isNetworkMessage = false) { if (CharacterHealth.Unkillable || GodMode || IsDead) { return; } @@ -3724,6 +3833,7 @@ namespace Barotrauma foreach (var joint in AnimController.LimbJoints) { + if (joint.LimbA.type == LimbType.Head || joint.LimbB.type == LimbType.Head) { continue; } if (joint.revoluteJoint != null) { joint.revoluteJoint.LimitEnabled = false; @@ -3779,6 +3889,9 @@ namespace Barotrauma causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); OnDeath?.Invoke(this, CauseOfDeath); + var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller); + if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); @@ -3786,13 +3899,20 @@ namespace Barotrauma KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); - if (info != null) { info.CauseOfDeath = CauseOfDeath; } + if (info != null) + { + info.CauseOfDeath = CauseOfDeath; + info.MissionsCompletedSinceDeath = 0; + } AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - foreach (Item heldItem in HeldItems.ToList()) + if (!LockHands) { - heldItem.Drop(this); + foreach (Item heldItem in HeldItems.ToList()) + { + heldItem.Drop(this); + } } SelectedConstruction = null; @@ -3809,13 +3929,18 @@ namespace Barotrauma if (GameMain.GameSession != null) { + if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant) + { + GameMain.GameSession.Campaign.CrewHasDied = true; + } + GameMain.GameSession.KillCharacter(this); } } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); - public void Revive() + public void Revive(bool removeAllAfflictions = true) { if (Removed) { @@ -3826,7 +3951,14 @@ namespace Barotrauma aiTarget?.Remove(); aiTarget = new AITarget(this); - CharacterHealth.RemoveAllAfflictions(); + if (removeAllAfflictions) + { + CharacterHealth.RemoveAllAfflictions(); + } + else + { + CharacterHealth.RemoveNegativeAfflictions(); + } SetAllDamage(0.0f, 0.0f, 0.0f); Oxygen = 100.0f; Bloodloss = 0.0f; @@ -3847,7 +3979,10 @@ namespace Barotrauma foreach (Limb limb in AnimController.Limbs) { #if CLIENT - if (limb.LightSource != null) limb.LightSource.Color = limb.InitialLightSourceColor; + if (limb.LightSource != null) + { + limb.LightSource.Color = limb.InitialLightSourceColor; + } #endif limb.body.Enabled = true; limb.IsSevered = false; @@ -4169,12 +4304,8 @@ namespace Barotrauma } if (Submarine == null && target.Submarine != null) { - if (AIController == null || !(AIController.SteeringManager is IndoorsSteeringManager)) - { - // outside and targeting inside - // doesn't work with inside steering - targetPos += target.Submarine.SimPosition; - } + // outside and targeting inside + targetPos += target.Submarine.SimPosition; } else if (Submarine != null && target.Submarine == null) { @@ -4208,8 +4339,277 @@ namespace Barotrauma public bool IsProtectedFromPressure() { - return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); + return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); } + + // Talent logic begins here. Should be encapsulated to its own controller soon + + private readonly List characterTalents = new List(); + + public void LoadTalents() + { + List toBeRemoved = null; + foreach (string talent in info.UnlockedTalents) + { + if (!GiveTalent(talent, addingFirstTime: false)) + { + DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo."); + toBeRemoved ??= new List(); + toBeRemoved.Add(talent); + } + } + + if (toBeRemoved != null) + { + foreach (string removeTalent in toBeRemoved) + { + Info.UnlockedTalents.Remove(removeTalent); + } + } + } + + public bool GiveTalent(string talentIdentifier, bool addingFirstTime = true) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talentIdentifier, StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); + return false; + } + return GiveTalent(talentPrefab, addingFirstTime); + } + + public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UIntIdentifier == talentIdentifier); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); + return false; + } + return GiveTalent(talentPrefab, addingFirstTime); + } + + public bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true) + { + if (info == null) { return false; } + info.UnlockedTalents.Add(talentPrefab.Identifier); + if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; } + +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); +#endif + CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); + characterTalent.ActivateTalent(addingFirstTime); + characterTalents.Add(characterTalent); + characterTalent.AddedThisRound = addingFirstTime; + + if (addingFirstTime) + { + OnTalentGiven(talentPrefab.Identifier); + } + return true; + } + + public bool HasTalent(string identifier) + { + return info.UnlockedTalents.Contains(identifier); + } + + public static IEnumerable GetFriendlyCrew(Character character) + { + return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); + } + + public bool HasTalents() + { + return characterTalents.Any(); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityObject); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, null); + } + } + + public bool HasRecipeForItem(string recipeIdentifier) + { + return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); + } + + /// + /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. + /// + public void GiveMoney(int amount) + { + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (amount <= 0) { return; } + + int prevAmount = campaign.Money; + campaign.Money += amount; + OnMoneyChanged(prevAmount, campaign.Money); + } + + public void SetMoney(int amount) + { + if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (amount == campaign.Money) { return; } + + int prevAmount = campaign.Money; + campaign.Money = amount; + OnMoneyChanged(prevAmount, campaign.Money); + } + + partial void OnMoneyChanged(int prevAmount, int newAmount); + partial void OnTalentGiven(string talentIdentifier); + + /// + /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. + /// If necessary, the approach of using a dictionary could be replaced by an encapsulated class that contains the stats as attributes. + /// + private readonly Dictionary statValues = new Dictionary(); + + /// + /// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking. + /// + private readonly Dictionary wearableStatValues = new Dictionary(); + + public float GetStatValue(StatTypes statType) + { + if (!IsHuman) { return 0f; } + + float statValue = 0f; + if (statValues.TryGetValue(statType, out float value)) + { + statValue += value; + } + if (CharacterHealth != null) + { + statValue += CharacterHealth.GetStatValue(statType); + } + if (Info != null) + { + // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change + statValue += Info.GetSavedStatValue(statType); + } + if (wearableStatValues.TryGetValue(statType, out float wearableValue)) + { + statValue += wearableValue; + } + + return statValue; + } + + public void OnWearablesChanged() + { + wearableStatValues.Clear(); + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand + && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) + { + foreach (var statValuePair in wearable.WearableStatValues) + { + if (wearableStatValues.ContainsKey(statValuePair.Key)) + { + wearableStatValues[statValuePair.Key] += statValuePair.Value; + } + else + { + wearableStatValues.Add(statValuePair.Key, statValuePair.Value); + } + } + } + } + } + + public void ChangeStat(StatTypes statType, float value) + { + if (statValues.ContainsKey(statType)) + { + statValues[statType] += value; + } + else + { + statValues.Add(statType, value); + } + } + + public static StatTypes GetSkillStatType(string skillIdentifier) + { + // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way + switch (skillIdentifier) + { + case "electrical": + return StatTypes.ElectricalSkillBonus; + case "helm": + return StatTypes.HelmSkillBonus; + case "mechanical": + return StatTypes.MechanicalSkillBonus; + case "medical": + return StatTypes.MedicalSkillBonus; + case "weapons": + return StatTypes.WeaponsSkillBonus; + default: + return StatTypes.None; + } + } + + private readonly List abilityFlags = new List(); + + public void AddAbilityFlag(AbilityFlags abilityFlag) + { + abilityFlags.Add(abilityFlag); + } + + public void RemoveAbilityFlag(AbilityFlags abilityFlag) + { + abilityFlags.Remove(abilityFlag); + } + + public bool HasAbilityFlag(AbilityFlags abilityFlag) + { + return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); + } + + private readonly Dictionary abilityResistances = new Dictionary(); + + public float GetAbilityResistance(AfflictionPrefab affliction) + { + return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f; + } + + public void ChangeAbilityResistance(string resistanceId, float value) + { + if (abilityResistances.ContainsKey(resistanceId)) + { + abilityResistances[resistanceId] *= value; + } + else + { + abilityResistances.Add(resistanceId, value); + } + } + + /// + /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs + /// + public bool IsFriendly(Character other) => IsFriendly(this, other); + + /// + /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs + /// + public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); } class ActiveTeamChange diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 14e7f34c5..c193a3918 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1,19 +1,19 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma { public enum Gender { None, Male, Female }; public enum Race { None, White, Black, Brown, Asian }; - // TODO: Generating the HeadInfo could be simplified. partial class CharacterInfo { public class HeadInfo @@ -24,15 +24,7 @@ namespace Barotrauma get { return _headSpriteId; } set { - _headSpriteId = value; - if (_headSpriteId < (int)headSpriteRange.X) - { - _headSpriteId = (int)headSpriteRange.Y; - } - if (_headSpriteId > (int)headSpriteRange.Y) - { - _headSpriteId = (int)headSpriteRange.X; - } + _headSpriteId = Math.Max(Math.Clamp(value, (int)headSpriteRange.X, (int)headSpriteRange.Y), 1); GetSpriteSheetIndex(); } } @@ -41,6 +33,10 @@ namespace Barotrauma public Gender gender; public Race race; + public Color HairColor; + public Color FacialHairColor; + public Color SkinColor; + public int HairIndex { get; set; } = -1; public int BeardIndex { get; set; } = -1; public int MoustacheIndex { get; set; } = -1; @@ -73,11 +69,11 @@ namespace Barotrauma FaceAttachmentIndex = -1; } - private void GetSpriteSheetIndex() + public void GetSpriteSheetIndex() { if (heads != null && heads.Any()) { - var matchingHead = heads.Keys.FirstOrDefault(h => h.Gender == gender && h.Race == race && h.ID == _headSpriteId); + var matchingHead = heads.Keys.FirstOrDefault(h => h.ID == HeadSpriteId && IsMatchingGender(h.Gender, gender) && IsMatchingRace(h.Race, race)); if (matchingHead != null) { if (heads.TryGetValue(matchingHead, out Vector2 index)) @@ -98,14 +94,13 @@ namespace Barotrauma if (head != value && value != null) { head = value; - if (head.race == Race.None) + if (!IsValidRace(head.race)) { head.race = GetRandomRace(Rand.RandSync.Unsynced); } CalculateHeadSpriteRange(); Head.HeadSpriteId = value.HeadSpriteId; - HeadSprite = null; - AttachmentSprites = null; + RefreshHeadSprites(); } } } @@ -156,7 +151,9 @@ namespace Barotrauma public bool HasNickname => Name != OriginalName; public string OriginalName { get; private set; } + public string Name; + public string DisplayName { get @@ -215,30 +212,56 @@ namespace Barotrauma public int Salary; - private Sprite headSprite; + public int ExperiencePoints { get; private set; } + + public HashSet UnlockedTalents { get; private set; } = new HashSet(); + + /// + /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection + /// + public IEnumerable GetUnlockedTalentsInTree() + { + if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + + return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t)); + } + + /// + /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them + /// + public IEnumerable GetEndocrineTalents() + { + if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + + return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t)); + } + + public int AdditionalTalentPoints { get; set; } + + private Sprite _headSprite; public Sprite HeadSprite { get { - if (headSprite == null) + if (_headSprite == null) { LoadHeadSprite(); } #if CLIENT - if (headSprite != null) + if (_headSprite != null) { - CalculateHeadPosition(headSprite); + CalculateHeadPosition(_headSprite); } #endif - return headSprite; + return _headSprite; } private set { - if (headSprite != null) + if (_headSprite != null) { - headSprite.Remove(); + _headSprite.Remove(); } - headSprite = value; + _headSprite = value; } } @@ -284,6 +307,7 @@ namespace Barotrauma Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f)); } + idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); if (idCard != null) { #if CLIENT @@ -291,19 +315,6 @@ namespace Barotrauma #endif return; } - - if (Character.Inventory != null) - { - idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); - if (idCard != null) - { -#if CLIENT - GetDisguisedSprites(idCard); -#endif - return; - } - - } } #if CLIENT @@ -380,29 +391,31 @@ namespace Barotrauma set { Head.HeadSpriteId = value; - HeadSprite = null; - AttachmentSprites = null; ResetHeadAttachments(); + RefreshHeadSprites(); } } public readonly bool HasGenders; + public readonly bool HasRaces; public Gender Gender { get { return Head.gender; } set { - if (Head.gender == value) return; + Gender previousValue = Head.gender; Head.gender = value; - if (Head.gender == Gender.None) + if (!IsValidGender(Head.gender)) { - Head.gender = Gender.Male; + Head.gender = GetDefaultGender(); + } + if (Head.gender != previousValue) + { + CalculateHeadSpriteRange(); + ResetHeadAttachments(); + RefreshHeadSprites(); } - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - HeadSprite = null; - AttachmentSprites = null; } } @@ -411,28 +424,82 @@ namespace Barotrauma get { return Head.race; } set { - if (Head.race == value) { return; } + Race previousValue = Head.race; Head.race = value; - if (Head.race == Race.None) + if (!IsValidRace(Head.race)) { - Head.race = Race.White; + Head.race = GetDefaultRace(); + } + if (Head.race != previousValue) + { + CalculateHeadSpriteRange(); + ResetHeadAttachments(); + RefreshHeadSprites(); } - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - HeadSprite = null; - AttachmentSprites = null; } } - public int HairIndex { get => Head.HairIndex; set => Head.HairIndex = value; } - public int BeardIndex { get => Head.BeardIndex; set => Head.BeardIndex = value; } - public int MoustacheIndex { get => Head.MoustacheIndex; set => Head.MoustacheIndex = value; } - public int FaceAttachmentIndex { get => Head.FaceAttachmentIndex; set => Head.FaceAttachmentIndex = value; } + private bool IsValidRace(Race race) => HasRaces ? race != Race.None : race == Race.None; - public XElement HairElement { get => Head.HairElement; set => Head.HairElement = value; } - public XElement BeardElement { get => Head.BeardElement; set => Head.BeardElement = value; } - public XElement MoustacheElement { get => Head.MoustacheElement; set => Head.MoustacheElement = value; } - public XElement FaceAttachment { get => Head.FaceAttachment; set => Head.FaceAttachment = value; } + private bool IsValidGender(Gender gender) => HasGenders ? gender != Gender.None : gender == Gender.None; + + private Gender GetDefaultGender() => HasGenders ? Gender.Male : Gender.None; + + private Race GetDefaultRace() => HasRaces ? Race.White : Race.None; + + public int HairIndex + { + get => Head.HairIndex; + set => Head.HairIndex = value; + } + + public int BeardIndex + { + get => Head.BeardIndex; + set => Head.BeardIndex = value; + } + + public int MoustacheIndex + { + get => Head.MoustacheIndex; + set => Head.MoustacheIndex = value; + } + + public int FaceAttachmentIndex + { + get => Head.FaceAttachmentIndex; + set => Head.FaceAttachmentIndex = value; + } + + public readonly ImmutableArray<(Color Color, float Commonness)> HairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; + + public Color HairColor + { + get => Head.HairColor; + set => Head.HairColor = value; + } + + public Color FacialHairColor + { + get => Head.FacialHairColor; + set => Head.FacialHairColor = value; + } + + public Color SkinColor + { + get => Head.SkinColor; + set => Head.SkinColor = value; + } + + public XElement HairElement => Head.HairElement; + + public XElement BeardElement => Head.BeardElement; + + public XElement MoustacheElement => Head.MoustacheElement; + + public XElement FaceAttachment => Head.FaceAttachment; private RagdollParams ragdoll; public RagdollParams Ragdoll @@ -455,6 +522,9 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; + // talent-relevant values + public int MissionsCompletedSinceDeath = 0; + // Used for creating the data public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "") { @@ -470,16 +540,15 @@ namespace Barotrauma if (doc == null) { return; } CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; // TODO: support for variants - head = new HeadInfo(); + Head = new HeadInfo(); HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - if (HasGenders) - { - Head.gender = GetRandomGender(randSync); - } - Head.race = GetRandomRace(randSync); - CalculateHeadSpriteRange(); - Head.HeadSpriteId = GetRandomHeadID(randSync); + HasRaces = CharacterConfigElement.GetAttributeBool("races", false); + SetGenderAndRace(randSync); Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); + SetColors(); if (!string.IsNullOrEmpty(name)) { @@ -492,23 +561,7 @@ namespace Barotrauma else { name = ""; - if (CharacterConfigElement.Element("name") != null) - { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") - { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - Name = ToolBox.GetRandomLine(firstNamePath, randSync); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (Name != "") Name += " "; - Name += ToolBox.GetRandomLine(lastNamePath, randSync); - } - } + Name = GetRandomName(randSync); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); @@ -520,6 +573,65 @@ namespace Barotrauma LoadHeadAttachments(); } + public string GetRandomName(Rand.RandSync randSync) + { + string name = ""; + if (CharacterConfigElement.Element("name") != null) + { + string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); + if (firstNamePath != "") + { + firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + name = ToolBox.GetRandomLine(firstNamePath, randSync); + } + + string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); + if (lastNamePath != "") + { + lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + if (name != "") { name += " "; } + name += ToolBox.GetRandomLine(lastNamePath, randSync); + } + } + + return name; + } + + public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array) + => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), Rand.RandSync.Unsynced) + .Color; + + private void SetGenderAndRace(Rand.RandSync randSync) + { + Head.gender = GetRandomGender(randSync); + Head.race = GetRandomRace(randSync); + CalculateHeadSpriteRange(); + HeadSpriteId = GetRandomHeadID(randSync); + } + + private void SetColors() + { + HairColor = SelectRandomColor(HairColors); + FacialHairColor = SelectRandomColor(FacialHairColors); + SkinColor = SelectRandomColor(SkinColors); + } + + private void CheckColors() + { + if (HairColor == Color.Black) + { + HairColor = SelectRandomColor(HairColors); + } + if (FacialHairColor == Color.Black) + { + FacialHairColor = SelectRandomColor(FacialHairColors); + } + if (SkinColor == Color.Black) + { + SkinColor = SelectRandomColor(SkinColors); + } + } + // Used for loading the data public CharacterInfo(XElement infoElement) { @@ -527,9 +639,11 @@ namespace Barotrauma idCounter++; Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); - string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant(); Salary = infoElement.GetAttributeInt("salary", 1000); - Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race); + ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); + UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); + AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); + Enum.TryParse(infoElement.GetAttributeString("race", "None"), true, out Race race); Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); _speciesName = infoElement.GetAttributeString("speciesname", null); XDocument doc = null; @@ -547,14 +661,19 @@ namespace Barotrauma // TODO: support for variants CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - if (HasGenders && gender == Gender.None) + HasRaces = CharacterConfigElement.GetAttributeBool("hasraces", false); + if (!IsValidGender(gender)) { gender = GetRandomGender(Rand.RandSync.Unsynced); } - else if (!HasGenders) + if (!IsValidRace(race)) { - gender = Gender.None; + race = GetRandomRace(Rand.RandSync.Unsynced); } + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); + RecreateHead( infoElement.GetAttributeInt("headspriteid", 1), race, @@ -564,6 +683,37 @@ namespace Barotrauma infoElement.GetAttributeInt("moustacheindex", -1), infoElement.GetAttributeInt("faceattachmentindex", -1)); + //backwards compatibility + if (infoElement.Attribute("skincolor") == null && infoElement.Attribute("race") != null) + { + string raceStr = infoElement.GetAttributeString("race", string.Empty); + Race obsoleteRace = Race.None; + Enum.TryParse(raceStr, ignoreCase: true, out obsoleteRace); + switch (obsoleteRace) + { + case Race.White: + case Race.None: + SkinColor = new Color(255, 215, 200, 255); + break; + case Race.Brown: + SkinColor = new Color(158, 95, 72, 255); + break; + case Race.Black: + SkinColor = new Color(153, 75, 42, 255); + break; + case Race.Asian: + SkinColor = new Color(191, 116, 61, 255); + break; + } + } + else + { + SkinColor = infoElement.GetAttributeColor("skincolor", Color.Black); + } + HairColor = infoElement.GetAttributeColor("haircolor", Color.Black); + FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Black); + CheckColors(); + if (string.IsNullOrEmpty(Name)) { if (CharacterConfigElement.Element("name") != null) @@ -596,20 +746,68 @@ namespace Barotrauma if (!string.IsNullOrEmpty(personalityName)) { personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); - } + } + + MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); + foreach (XElement subElement in infoElement.Elements()) { - if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) + bool jobCreated = false; + if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) { Job = new Job(subElement); - break; + jobCreated = true; + // there used to be a break here, but it had to be removed to make room for statvalues + // using the jobCreated boolean to make sure that only the first job found is created + } + else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase)) + { + foreach (XElement savedStat in subElement.Elements()) + { + string statTypeString = savedStat.GetAttributeString("stattype", "").ToLowerInvariant(); + if (!Enum.TryParse(statTypeString, true, out StatTypes statType)) + { + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" when loading character data in CharacterInfo!"); + continue; + } + + float value = savedStat.GetAttributeFloat("statvalue", 0f); + if (value == 0f) { continue; } + + string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant(); + if (string.IsNullOrEmpty(statIdentifier)) + { + DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!"); + return; + } + + bool removeOnDeath = savedStat.GetAttributeBool("removeondeath", true); + ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); + } } } LoadHeadAttachments(); } - public Gender GetRandomGender(Rand.RandSync randSync) => (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; - public Race GetRandomRace(Rand.RandSync randSync) => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); + public Gender GetRandomGender(Rand.RandSync randSync) + { + if (HasGenders) + { + return (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; + } + return Gender.None; + } + + public Race GetRandomRace(Rand.RandSync randSync) + { + if (HasRaces) + { + return new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); + } + return Race.None; + } + + public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0; private List hairs; @@ -676,10 +874,13 @@ namespace Barotrauma { if (elements == null) { return elements; } return elements.Where(w => - Enum.TryParse(w.GetAttributeString("gender", "None"), true, out Gender g) && g == gender && - Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race); + IsMatchingGender(Enum.Parse(w.GetAttributeString("gender", "None"), ignoreCase: true), gender) && + IsMatchingRace(Enum.Parse(w.GetAttributeString("race", "None"), ignoreCase: true), race)); } + public static bool IsMatchingGender(Gender gender, Gender myGender) => gender == Gender.None || gender == myGender; + public static bool IsMatchingRace(Race race, Race myRace) => race == Race.None || race == myRace; + private void LoadHeadPresets() { if (CharacterConfigElement == null) { return; } @@ -708,9 +909,16 @@ namespace Barotrauma // If there are any head presets defined, use them. if (heads.Any()) { - var ids = heads.Keys.Where(h => h.Race == Race && h.Gender == Gender).Select(w => w.ID); + var ids = heads.Keys.Where(h => IsMatchingRace(Race, h.Race) && IsMatchingGender(Gender, h.Gender)).Select(w => w.ID); ids = ids.OrderBy(id => id); - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); + if (ids.Any()) + { + Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); + } + else + { + DebugConsole.ThrowError($"[CharacterInfo] Couldn't find a head definition that matches {Race} and {Gender}!"); + } } // Else we calculate the range from the wearables. if (Head.headSpriteRange == Vector2.Zero) @@ -743,26 +951,72 @@ namespace Barotrauma } } + public void RecreateHead(HeadInfo headInfo) + { + RecreateHead( + headInfo.HeadSpriteId, + headInfo.race, + headInfo.gender, + headInfo.HairIndex, + headInfo.BeardIndex, + headInfo.MoustacheIndex, + headInfo.FaceAttachmentIndex); + + SkinColor = headInfo.SkinColor; + HairColor = headInfo.HairColor; + FacialHairColor = headInfo.FacialHairColor; + CheckColors(); + } + + /// + /// Recreates the head info and checks that everything is valid. + /// public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { - if (HasGenders && gender == Gender.None) + if (!IsValidGender(gender)) { gender = GetRandomGender(Rand.RandSync.Unsynced); } - else if (!HasGenders) + if (!IsValidRace(race)) { - gender = Gender.None; + race = GetRandomRace(Rand.RandSync.Unsynced); } if (heads == null) { LoadHeadPresets(); } - head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + Color skin = Color.Black; + Color hair = Color.Black; + Color facialHair = Color.Black; + if (head != null) + { + skin = head.SkinColor; + hair = head.HairColor; + facialHair = head.FacialHairColor; + } + head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex) + { + SkinColor = skin, + HairColor = hair, + FacialHairColor = facialHair + }; CalculateHeadSpriteRange(); ReloadHeadAttachments(); + RefreshHead(); } - public void LoadHeadSprite() + /// + /// Reloads the head sprite and the attachment sprites. + /// + public void RefreshHead() + { + ReloadHeadAttachments(); + RefreshHeadSprites(); + } + + partial void LoadHeadSpriteProjectSpecific(XElement limbElement); + + private void LoadHeadSprite() { foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { @@ -772,6 +1026,7 @@ namespace Barotrauma if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; + if (string.IsNullOrEmpty(spritePath)) { continue; } spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant()); @@ -779,6 +1034,8 @@ namespace Barotrauma string fileName = Path.GetFileNameWithoutExtension(spritePath); + if (string.IsNullOrEmpty(fileName)) { continue; } + //go through the files in the directory to find a matching sprite foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { @@ -803,13 +1060,12 @@ namespace Barotrauma break; } + LoadHeadSpriteProjectSpecific(limbElement); + break; } } - /// - /// Loads only the elements according to the indices, not the sprites. - /// public void LoadHeadAttachments() { if (Wearables != null) @@ -871,7 +1127,7 @@ namespace Barotrauma } } - private static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + public static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) { // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); @@ -880,9 +1136,9 @@ namespace Barotrauma return list; } - private XElement GetRandomElement(IEnumerable elements) + public XElement GetRandomElement(IEnumerable elements) { - var filtered = elements.Where(e => IsWearableAllowed(e)); + var filtered = elements.Where(IsWearableAllowed); if (filtered.Count() == 0) { return null; } var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); return element == null || element.Name == "Empty" ? null : element; @@ -907,7 +1163,7 @@ namespace Barotrauma return true; } - private static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + public static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); @@ -926,7 +1182,7 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } - public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 pos) + public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromApprenticeship = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -935,15 +1191,29 @@ namespace Barotrauma increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier; } + increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); + float prevLevel = Job.GetSkillLevel(skillIdentifier); - Job.IncreaseSkillLevel(skillIdentifier, increase); + Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); float newLevel = Job.GetSkillLevel(skillIdentifier); - OnSkillChanged(skillIdentifier, prevLevel, newLevel, pos); + if ((int)newLevel > (int)prevLevel) + { + // assume we are getting at least 1 point in skill, since this logic only runs in such cases + float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f); + var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromApprenticeship); + Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain); + foreach (Character character in Character.GetFriendlyCrew(Character)) + { + character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilitySkillGain); + } + } + + OnSkillChanged(skillIdentifier, prevLevel, newLevel); } - public void SetSkillLevel(string skillIdentifier, float level, Vector2 pos) + public void SetSkillLevel(string skillIdentifier, float level) { if (Job == null) { return; } @@ -951,17 +1221,102 @@ namespace Barotrauma if (skill == null) { Job.Skills.Add(new Skill(skillIdentifier, level)); - OnSkillChanged(skillIdentifier, 0.0f, level, pos); + OnSkillChanged(skillIdentifier, 0.0f, level); } else { float prevLevel = skill.Level; skill.Level = level; - OnSkillChanged(skillIdentifier, prevLevel, skill.Level, pos); + OnSkillChanged(skillIdentifier, prevLevel, skill.Level); } } - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos); + partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel); + + public void GiveExperience(int amount, bool isMissionExperience = false) + { + int prevAmount = ExperiencePoints; + + var experienceGainMultiplier = new AbilityValue(1f); + if (isMissionExperience) + { + Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); + } + experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0; + + amount = (int)(amount * experienceGainMultiplier.Value); + if (amount < 0) { return; } + + ExperiencePoints += amount; + OnExperienceChanged(prevAmount, ExperiencePoints); + } + + public void SetExperience(int newExperience) + { + if (newExperience < 0) { return; } + + int prevAmount = ExperiencePoints; + ExperiencePoints = newExperience; + OnExperienceChanged(prevAmount, ExperiencePoints); + } + + const int BaseExperienceRequired = -50; + const int AddedExperienceRequiredPerLevel = 500; + + public int GetTotalTalentPoints() + { + return GetCurrentLevel() + AdditionalTalentPoints - 1; + } + + public int GetAvailableTalentPoints() + { + // hashset always has at least 1 + return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0); + } + + public float GetProgressTowardsNextLevel() + { + float progress = (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel()); + return progress; + } + + public float GetExperienceRequiredForCurrentLevel() + { + GetCurrentLevel(out int experienceRequired); + return experienceRequired; + } + + public float GetExperienceRequiredToLevelUp() + { + int level = GetCurrentLevel(out int experienceRequired); + return experienceRequired + ExperienceRequiredPerLevel(level); + } + + public int GetCurrentLevel() + { + return GetCurrentLevel(out _); + } + + private int GetCurrentLevel(out int experienceRequired) + { + int level = 1; + experienceRequired = 0; + while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints) + { + experienceRequired += ExperienceRequiredPerLevel(level); + level++; + } + return level; + } + + private int ExperienceRequiredPerLevel(int level) + { + return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level; + } + + partial void OnExperienceChanged(int prevAmount, int newAmount); + + partial void OnPermanentStatChanged(StatTypes statType); public void Rename(string newName) { @@ -996,20 +1351,27 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("originalname", OriginalName), new XAttribute("speciesname", SpeciesName), - new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"), + new XAttribute("gender", Head.gender.ToString()), new XAttribute("race", Head.race.ToString()), new XAttribute("salary", Salary), + new XAttribute("experiencepoints", ExperiencePoints), + new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), + new XAttribute("additionaltalentpoints", AdditionalTalentPoints), new XAttribute("headspriteid", HeadSpriteId), new XAttribute("hairindex", HairIndex), new XAttribute("beardindex", BeardIndex), new XAttribute("moustacheindex", MoustacheIndex), new XAttribute("faceattachmentindex", FaceAttachmentIndex), + new XAttribute("skincolor", XMLExtensions.ColorToString(SkinColor)), + new XAttribute("haircolor", XMLExtensions.ColorToString(HairColor)), + new XAttribute("facialhaircolor", XMLExtensions.ColorToString(FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); - // TODO: animations? + charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); + if (Character != null) { if (Character.AnimController.CurrentHull != null) @@ -1020,6 +1382,27 @@ namespace Barotrauma Job.Save(charElement); + XElement savedStatElement = new XElement("savedstatvalues"); + foreach (var statValuePair in SavedStatValues) + { + foreach (var savedStat in statValuePair.Value) + { + if (savedStat.StatValue == 0f) { continue; } + if (savedStat.RemoveAfterRound) { continue; } + + savedStatElement.Add(new XElement("savedstatvalue", + new XAttribute("stattype", statValuePair.Key.ToString()), + new XAttribute("statidentifier", savedStat.StatIdentifier), + new XAttribute("statvalue", savedStat.StatValue), + new XAttribute("removeondeath", savedStat.RemoveOnDeath) + )); + } + } + + + + charElement.Add(savedStatElement); + parentElement.Add(charElement); return charElement; } @@ -1295,13 +1678,19 @@ namespace Barotrauma if (healthData != null) { character?.CharacterHealth.Load(healthData); } } - public void ReloadHeadAttachments() + /// + /// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites. + /// + private void ReloadHeadAttachments() { ResetLoadedAttachments(); LoadHeadAttachments(); } - public void ResetHeadAttachments() + /// + /// Loads only the elements according to the indices, not the sprites. + /// + private void ResetHeadAttachments() { ResetAttachmentIndices(); ResetLoadedAttachments(); @@ -1332,5 +1721,121 @@ namespace Barotrauma Portrait = null; AttachmentSprites = null; } + + private void RefreshHeadSprites() + { + HeadSprite = null; + AttachmentSprites = null; + } + + // This could maybe be a LookUp instead? + public readonly Dictionary> SavedStatValues = new Dictionary>(); + + public void ClearSavedStatValues() + { + foreach (StatTypes statType in SavedStatValues.Keys) + { + OnPermanentStatChanged(statType); + } + SavedStatValues.Clear(); + } + + public void ClearSavedStatValues(StatTypes statType) + { + SavedStatValues.Remove(statType); + OnPermanentStatChanged(statType); + } + + public void ResetSavedStatValue(string statIdentifier) + { + foreach (StatTypes statType in SavedStatValues.Keys) + { + bool changed = false; + foreach (SavedStatValue savedStatValue in SavedStatValues[statType]) + { + if (savedStatValue.StatIdentifier != statIdentifier) { continue; } + if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; } + savedStatValue.StatValue = 0.0f; + changed = true; + } + if (changed) { OnPermanentStatChanged(statType); } + } + } + + public float GetSavedStatValue(StatTypes statType) + { + if (SavedStatValues.TryGetValue(statType, out var statValues)) + { + return statValues.Sum(v => v.StatValue); + } + else + { + return 0f; + } + } + public float GetSavedStatValue(StatTypes statType, string statIdentifier) + { + if (SavedStatValues.TryGetValue(statType, out var statValues)) + { + return statValues.Where(s => s.StatIdentifier.Equals(statIdentifier, StringComparison.OrdinalIgnoreCase)).Sum(v => v.StatValue); + } + else + { + return 0f; + } + } + + public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue, bool setValue = false) + { + if (!SavedStatValues.ContainsKey(statType)) + { + SavedStatValues.Add(statType, new List()); + } + + bool changed = false; + if (SavedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat) + { + float prevValue = savedStat.StatValue; + savedStat.StatValue = setValue ? value : MathHelper.Min(savedStat.StatValue + value, maxValue); + changed = !MathUtils.NearlyEqual(savedStat.StatValue, prevValue); + } + else + { + SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath, removeAfterRound)); + changed = true; + } + if (changed) { OnPermanentStatChanged(statType); } + } + } + + public class SavedStatValue + { + public string StatIdentifier { get; set; } + public float StatValue { get; set; } + public bool RemoveOnDeath { get; set; } + public bool RemoveAfterRound { get; set; } + + public SavedStatValue(string statIdentifier, float value, bool removeOnDeath, bool retainAfterRound) + { + StatValue = value; + RemoveOnDeath = removeOnDeath; + StatIdentifier = statIdentifier; + RemoveAfterRound = retainAfterRound; + } + } + + class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter + { + public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromApprenticeship) + { + Value = value; + String = abilityString; + Character = character; + GainedFromApprenticeship = gainedFromApprenticeship; + } + public Character Character { get; set; } + public float Value { get; set; } + public string String { get; set; } + public bool GainedFromApprenticeship { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 95e472082..ab825d986 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -17,6 +17,8 @@ namespace Barotrauma public float PendingAdditionStrength { get; set; } public float AdditionStrength { get; set; } + private float fluctuationTimer; + protected float _strength; [Serialize(0f, true), Editable] @@ -56,6 +58,8 @@ namespace Barotrauma public readonly Dictionary PeriodicEffectTimers = new Dictionary(); + public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + /// /// Which character gave this affliction /// @@ -123,7 +127,7 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinGrainStrength, currentEffect.MaxGrainStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); if (Prefab.GrainBurst > 0 && AdditionStrength > amount) { @@ -138,12 +142,12 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxScreenDistort - currentEffect.MinScreenDistort < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinScreenDistortStrength, - currentEffect.MaxScreenDistortStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinScreenDistort, + currentEffect.MaxScreenDistort, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); } public float GetRadialDistortStrength() @@ -151,12 +155,12 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxRadialDistort - currentEffect.MinRadialDistort < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinRadialDistortStrength, - currentEffect.MaxRadialDistortStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinRadialDistort, + currentEffect.MaxRadialDistort, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); } public float GetChromaticAberrationStrength() @@ -164,11 +168,50 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxChromaticAberration - currentEffect.MinChromaticAberration < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinChromaticAberrationStrength, - currentEffect.MaxChromaticAberrationStrength, + currentEffect.MinChromaticAberration, + currentEffect.MaxChromaticAberration, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + } + + public float GetAfflictionOverlayMultiplier() + { + //If the overlay's alpha progresses linearly, then don't worry about affliction effects. + if (Prefab.AfflictionOverlayAlphaIsLinear) { return (Strength / Prefab.MaxStrength); } + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxAfflictionOverlayAlphaMultiplier - currentEffect.MinAfflictionOverlayAlphaMultiplier < 0.0f) { return 0.0f; } + + return MathHelper.Lerp( + currentEffect.MinAfflictionOverlayAlphaMultiplier, + currentEffect.MaxAfflictionOverlayAlphaMultiplier, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + + public Color GetFaceTint() + { + if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return Color.TransparentBlack; } + + return Color.Lerp( + currentEffect.MinFaceTint, + currentEffect.MaxFaceTint, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + + public Color GetBodyTint() + { + if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return Color.TransparentBlack; } + + return Color.Lerp( + currentEffect.MinBodyTint, + currentEffect.MaxBodyTint, (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); } @@ -177,12 +220,18 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxScreenBlur - currentEffect.MinScreenBlur < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinScreenBlurStrength, - currentEffect.MaxScreenBlurStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinScreenBlur, + currentEffect.MaxScreenBlur, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + } + + private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect) + { + if (currentEffect == null || currentEffect.ScreenEffectFluctuationFrequency <= 0.0f) { return 1.0f; } + return ((float)Math.Sin(fluctuationTimer * MathHelper.TwoPi) + 1.0f) * 0.5f; } public float GetSkillMultiplier() @@ -210,14 +259,17 @@ namespace Barotrauma } } - public float GetResistance(string afflictionId) + public float GetResistance(AfflictionPrefab affliction) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) { return 0.0f; } - if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) { return 0.0f; } - + if (!currentEffect.ResistanceFor.Any(r => + r.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || + r.Equals(affliction.AfflictionType, StringComparison.OrdinalIgnoreCase))) + { + return 0.0f; + } return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, @@ -229,14 +281,39 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 1.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 1.0f; } - if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) { return 1.0f; } - return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, currentEffect.MaxSpeedMultiplier, (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); } + public float GetStatValue(StatTypes statType) + { + if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + + if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) + { + return MathHelper.Lerp( + value.minValue, + value.maxValue, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + return 0.0f; + } + + public bool HasFlag(AbilityFlags flagType) + { + if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + + return currentEffect.AfflictionAbilityFlags.Contains(flagType); + } + + private AfflictionPrefab.Effect GetViableEffect() + { + if (Strength < Prefab.ActivationThreshold) { return null; } + return GetActiveEffect(); + } + public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects) @@ -262,13 +339,20 @@ namespace Barotrauma AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return; } + fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency; + fluctuationTimer %= 1.0f; + if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted { - _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier; + float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) + : characterHealth.Character.GetStatValue(StatTypes.DebuffDurationMultiplier))); + + _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; + } - else // Reduce strengthening of afflictions if resistant + else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { - _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); + _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab)); } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); @@ -309,6 +393,8 @@ namespace Barotrauma private readonly List targets = new List(); public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) { + if (type == ActionType.OnDamaged && !statusEffect.HasRequiredAfflictions(characterHealth.Character.LastDamage)) { return; } + statusEffect.SetUser(Source); if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 10907fe96..3878de39b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Xml.Linq; using System; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -21,6 +22,8 @@ namespace Barotrauma private Character character; + private bool stun = true; + private readonly List huskInfection = new List(); [Serialize(0f, true), Editable] @@ -34,6 +37,11 @@ namespace Barotrauma float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; float max = Math.Max(threshold, previousValue); _strength = Math.Clamp(value, 0, max); + stun = GameMain.GameSession?.IsRunning ?? true; + if (previousValue > 0.0f && value <= 0.0f) + { + DeactivateHusk(); + } } } @@ -51,8 +59,12 @@ namespace Barotrauma } } - private float DormantThreshold => Prefab.MaxStrength * 0.5f; - private float ActiveThreshold => Prefab.MaxStrength * 0.75f; + private float DormantThreshold => (Prefab as AfflictionPrefabHusk)?.DormantThreshold ?? Prefab.MaxStrength * 0.5f; + private float ActiveThreshold => (Prefab as AfflictionPrefabHusk)?.ActiveThreshold ?? Prefab.MaxStrength * 0.75f; + + private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f; + + private float TransformThresholdOnDeath => (Prefab as AfflictionPrefabHusk)?.TransformThresholdOnDeath ?? ActiveThreshold; public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } @@ -83,9 +95,9 @@ namespace Barotrauma } State = InfectionState.Transition; } - else if (Strength < Prefab.MaxStrength) + else if (Strength < TransitionThreshold) { - if (State != InfectionState.Active) + if (State != InfectionState.Active && stun) { character.SetStun(Rand.Range(2, 4)); } @@ -139,6 +151,7 @@ namespace Barotrauma private void DeactivateHusk() { + if (character?.AnimController == null || character.Removed) { return; } if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) { character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); @@ -161,7 +174,7 @@ namespace Barotrauma private void CharacterDead(Character character, CauseOfDeath causeOfDeath) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (Strength < ActiveThreshold || character.Removed) + if (Strength < TransformThresholdOnDeath || character.Removed) { UnsubscribeFromDeathEvent(); return; @@ -205,6 +218,14 @@ namespace Barotrauma XElement parentElement = new XElement("CharacterInfo"); XElement infoElement = character.Info?.Save(parentElement); CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement); + + if (huskCharacterInfo != null) + { + var bodyTint = GetBodyTint(); + huskCharacterInfo.SkinColor = + Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); + } + var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true); if (husk.Info != null) { @@ -212,6 +233,25 @@ namespace Barotrauma husk.Info.TeamID = CharacterTeamType.None; } + if (Prefab is AfflictionPrefabHusk huskPrefab) + { + if (huskPrefab.ControlHusk) + { +#if SERVER + var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.CharacterInfo.Character == character); + if (client != null) + { + GameMain.Server.SetClientCharacter(client, husk); + } +#else + if (!character.IsRemotelyControlled && character == Character.Controlled) + { + Character.Controlled = husk; + } +#endif + } + } + foreach (Limb limb in husk.AnimController.Limbs) { if (limb.type == LimbType.None) @@ -229,15 +269,19 @@ namespace Barotrauma } } + if ((Prefab as AfflictionPrefabHusk)?.TransferBuffs ?? false) + { + foreach (Affliction affliction in character.CharacterHealth.Afflictions) + { + if (affliction.Prefab.IsBuff) + { + husk.CharacterHealth.ApplyAffliction(null, affliction.Prefab.Instantiate(affliction.Strength)); + } + } + } + if (character.Inventory != null && husk.Inventory != null) { - if (character.Inventory.Capacity != husk.Inventory.Capacity) - { - string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - yield return CoroutineStatus.Success; - } for (int i = 0; i < character.Inventory.Capacity && i < husk.Inventory.Capacity; i++) { character.Inventory.GetItemsAt(i).ForEachMod(item => husk.Inventory.TryPutItem(item, i, true, false, null)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index a056bfcf9..2b87f5573 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -1,10 +1,10 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Abilities; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Xml.Linq; -using System.Linq; -using System.Security.Cryptography; namespace Barotrauma { @@ -91,9 +91,16 @@ namespace Barotrauma AttachLimbType = LimbType.None; } + TransferBuffs = element.GetAttributeBool("transferbuffs", true); SendMessages = element.GetAttributeBool("sendmessages", true); CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true); NeedsAir = element.GetAttributeBool("needsair", false); + ControlHusk = element.GetAttributeBool("controlhusk", false); + + DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); + ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); + TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); + TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold); } // Use any of these to define which limb the appendage is attached to. @@ -102,13 +109,18 @@ namespace Barotrauma public readonly string AttachLimbName; public readonly LimbType AttachLimbType; + public float ActiveThreshold, DormantThreshold, TransitionThreshold; + public float TransformThresholdOnDeath; + public readonly string HuskedSpeciesName; public readonly string[] TargetSpecies; public const string Tag = "[speciesname]"; + public readonly bool TransferBuffs; public readonly bool SendMessages; public readonly bool CauseSpeechImpediment; public readonly bool NeedsAir; + public readonly bool ControlHusk; } partial class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier @@ -116,83 +128,123 @@ namespace Barotrauma public class Effect { //this effect is applied when the strength is within this range - public float MinStrength, MaxStrength; + [Serialize(0.0f, false)] + public float MinStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MinVitalityDecrease { get; private set; } + + [Serialize(0.0f, false)] + public float MaxVitalityDecrease { get; private set; } - public readonly float MinVitalityDecrease = 0.0f; - public readonly float MaxVitalityDecrease = 0.0f; - //how much the strength of the affliction changes per second - public readonly float StrengthChange = 0.0f; + [Serialize(0.0f, false)] + public float StrengthChange { get; private set; } - public readonly bool MultiplyByMaxVitality; + [Serialize(false, false)] + public bool MultiplyByMaxVitality { get; private set; } - public float MinScreenBlurStrength, MaxScreenBlurStrength; - public float MinScreenDistortStrength, MaxScreenDistortStrength; - public float MinGrainStrength, MaxGrainStrength; - public float MinRadialDistortStrength, MaxRadialDistortStrength; - public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength; - public float MinSpeedMultiplier, MaxSpeedMultiplier; - public float MinBuffMultiplier, MaxBuffMultiplier; + [Serialize(0.0f, false)] + public float MinScreenBlur { get; private set; } - public float MinSkillMultiplier, MaxSkillMultiplier; + [Serialize(0.0f, false)] + public float MaxScreenBlur { get; private set; } - public float MinResistance, MaxResistance; - public string ResistanceFor; - public string DialogFlag; + [Serialize(0.0f, false)] + public float MinScreenDistort { get; private set; } + + [Serialize(0.0f, false)] + public float MaxScreenDistort { get; private set; } + + [Serialize(0.0f, false)] + public float MinRadialDistort { get; private set; } + + [Serialize(0.0f, false)] + public float MaxRadialDistort { get; private set; } + + [Serialize(0.0f, false)] + public float MinChromaticAberration { get; private set; } + + [Serialize(0.0f, false)] + public float MaxChromaticAberration { get; private set; } + + [Serialize("255,255,255,255", false)] + public Color GrainColor { get; private set; } + + [Serialize(0.0f, false)] + public float MinGrainStrength { get; private set; } + + [Serialize(0.0f, false)] + public float MaxGrainStrength { get; private set; } + + [Serialize(0.0f, false)] + public float ScreenEffectFluctuationFrequency { get; private set; } + + [Serialize(1.0f, false)] + public float MinAfflictionOverlayAlphaMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MinBuffMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxBuffMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MinSpeedMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxSpeedMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MinSkillMultiplier { get; private set; } + + [Serialize(1.0f, false)] + public float MaxSkillMultiplier { get; private set; } + + private readonly string[] resistanceFor; + public IEnumerable ResistanceFor + { + get { return resistanceFor; } + } + + [Serialize(0.0f, false)] + public float MinResistance { get; private set; } + + [Serialize(0.0f, false)] + public float MaxResistance { get; private set; } + + [Serialize("", false)] + public string DialogFlag { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MinFaceTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MaxFaceTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MinBodyTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MaxBodyTint { get; private set; } + + public readonly Dictionary AfflictionStatValues = new Dictionary(); + public readonly HashSet AfflictionAbilityFlags = new HashSet(); //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); public Effect(XElement element, string parentDebugName) { - MinStrength = element.GetAttributeFloat("minstrength", 0); - MaxStrength = element.GetAttributeFloat("maxstrength", 0); + SerializableProperty.DeserializeProperties(this, element); - MultiplyByMaxVitality = element.GetAttributeBool("multiplybymaxvitality", false); - - MinVitalityDecrease = element.GetAttributeFloat("minvitalitydecrease", 0.0f); - MaxVitalityDecrease = element.GetAttributeFloat("maxvitalitydecrease", 0.0f); - MaxVitalityDecrease = Math.Max(MinVitalityDecrease, MaxVitalityDecrease); - - MinScreenDistortStrength = element.GetAttributeFloat("minscreendistort", 0.0f); - MaxScreenDistortStrength = element.GetAttributeFloat("maxscreendistort", 0.0f); - MaxScreenDistortStrength = Math.Max(MinScreenDistortStrength, MaxScreenDistortStrength); - - MinRadialDistortStrength = element.GetAttributeFloat("minradialdistort", 0.0f); - MaxRadialDistortStrength = element.GetAttributeFloat("maxradialdistort", 0.0f); - MaxRadialDistortStrength = Math.Max(MinRadialDistortStrength, MaxRadialDistortStrength); - - MinChromaticAberrationStrength = element.GetAttributeFloat("minchromaticaberration", 0.0f); - MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f); - MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength); - - MinGrainStrength = element.GetAttributeFloat(nameof(MinGrainStrength).ToLower(), 0.0f); - MaxGrainStrength = element.GetAttributeFloat(nameof(MaxGrainStrength).ToLower(), 0.0f); - MaxGrainStrength = Math.Max(MinGrainStrength, MaxGrainStrength); - - MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f); - MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f); - MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength); - - MinSkillMultiplier = element.GetAttributeFloat("minskillmultiplier", 1.0f); - MaxSkillMultiplier = element.GetAttributeFloat("maxskillmultiplier", 1.0f); - - ResistanceFor = element.GetAttributeString("resistancefor", ""); - MinResistance = element.GetAttributeFloat("minresistance", 0.0f); - MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f); - MaxResistance = Math.Max(MinResistance, MaxResistance); - - MinSpeedMultiplier = element.GetAttributeFloat("minspeedmultiplier", 1.0f); - MaxSpeedMultiplier = element.GetAttributeFloat("maxspeedmultiplier", 1.0f); - MaxSpeedMultiplier = Math.Max(MinSpeedMultiplier, MaxSpeedMultiplier); - - MinBuffMultiplier = element.GetAttributeFloat("minbuffmultiplier", 1.0f); - MaxBuffMultiplier = element.GetAttributeFloat("maxbuffmultiplier", 1.0f); - MaxBuffMultiplier = Math.Max(MinBuffMultiplier, MaxBuffMultiplier); - - DialogFlag = element.GetAttributeString("dialogflag", ""); - - StrengthChange = element.GetAttributeFloat("strengthchange", 0.0f); + resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true); foreach (XElement subElement in element.Elements()) { @@ -201,6 +253,19 @@ namespace Barotrauma case "statuseffect": StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; + case "statvalue": + var statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), parentDebugName); + + float defaultValue = subElement.GetAttributeFloat("value", 0f); + float minValue = subElement.GetAttributeFloat("minvalue", defaultValue); + float maxValue = subElement.GetAttributeFloat("maxvalue", defaultValue); + + AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); + break; + case "abilityflag": + var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); + AfflictionAbilityFlags.Add(flagType); + break; } } } @@ -317,6 +382,9 @@ namespace Barotrauma public readonly Sprite Icon; public readonly Color[] IconColors; + public readonly Sprite AfflictionOverlay; + public readonly bool AfflictionOverlayAlphaIsLinear; + private readonly List effects = new List(); private readonly List periodicEffects = new List(); @@ -576,6 +644,11 @@ namespace Barotrauma Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); + if (element.Attribute("nameidentifier") != null) + { + Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name; + } + LimbSpecific = element.GetAttributeBool("limbspecific", false); if (!LimbSpecific) { @@ -590,7 +663,7 @@ namespace Barotrauma ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); - GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLower(), 0.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); @@ -604,6 +677,7 @@ namespace Barotrauma SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); IconColors = element.GetAttributeColorArray("iconcolors", null); + AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); foreach (XElement subElement in element.Elements()) @@ -613,6 +687,18 @@ namespace Barotrauma case "icon": Icon = new Sprite(subElement); break; + case "afflictionoverlay": + AfflictionOverlay = new Sprite(subElement); + break; + case "statvalue": + DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects."); + break; + case "effect": + case "periodiceffect": + break; + default: + DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index bfa7d0906..eff3bba62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -7,6 +7,7 @@ using Barotrauma.Networking; using Barotrauma.Extensions; using System.Globalization; using MoonSharp.Interpreter; +using Barotrauma.Abilities; namespace Barotrauma { @@ -117,15 +118,15 @@ namespace Barotrauma private set => Character.Params.Health.CrushDepth = value; } - private List limbHealths = new List(); + private readonly List limbHealths = new List(); //non-limb-specific afflictions - private List afflictions = new List(); + private readonly List afflictions = new List(); /// /// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions. /// public IEnumerable Afflictions => afflictions; - private HashSet irremovableAfflictions = new HashSet(); + private readonly HashSet irremovableAfflictions = new HashSet(); private Affliction bloodlossAffliction; private Affliction oxygenLowAffliction; private Affliction pressureAffliction; @@ -133,7 +134,7 @@ namespace Barotrauma public bool IsUnconscious { - get { return Vitality <= 0.0f || Character.IsDead; } + get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); } } public float PressureKillDelay { get; private set; } = 5.0f; @@ -152,6 +153,7 @@ namespace Barotrauma max += Character.Info.Job.Prefab.VitalityModifier; } max *= Character.StaticHealthMultiplier; + max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier); return max * Character.HealthMultiplier; } } @@ -168,6 +170,20 @@ namespace Barotrauma } } + public Color DefaultFaceTint = Color.TransparentBlack; + + public Color FaceTint + { + get; + private set; + } + + public Color BodyTint + { + get; + private set; + } + public float OxygenAmount { get @@ -191,7 +207,11 @@ namespace Barotrauma public float Stun { get { return stunAffliction.Strength; } - set { stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); } + set + { + if (Character.GodMode) { return; } + stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); + } } public float StunTimer { get; private set; } @@ -266,6 +286,12 @@ namespace Barotrauma private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); + /// + /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. + /// + private IEnumerable GetMatchingAfflictions(LimbHealth limb) + => limb.Afflictions.Union(afflictions.Where(a => GetMatchingLimbHealth(a) == limb)); + /// /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. /// @@ -402,7 +428,7 @@ namespace Barotrauma return strength; } - public void ApplyAffliction(Limb targetLimb, Affliction affliction) + public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true) { if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; } if (affliction.Prefab.LimbSpecific) @@ -418,7 +444,7 @@ namespace Barotrauma //if a limb-specific affliction is applied to no specific limb, apply to all limbs foreach (LimbHealth limbHealth in limbHealths) { - AddLimbAffliction(limbHealth, affliction); + AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking); } } @@ -429,7 +455,7 @@ namespace Barotrauma if (should.Bool()) return; - AddLimbAffliction(targetLimb, affliction); + AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking); } } else @@ -439,25 +465,41 @@ namespace Barotrauma if (should.Bool()) return; - AddAffliction(affliction); + AddAffliction(affliction, allowStacking: allowStacking); } } - public float GetResistance(string resistanceId) + public float GetResistance(AfflictionPrefab affliction) { float resistance = 0.0f; for (int i = 0; i < afflictions.Count; i++) { - if (!afflictions[i].Prefab.IsBuff) continue; - float temp = afflictions[i].GetResistance(resistanceId); - if (temp > resistance) resistance = temp; + resistance += afflictions[i].GetResistance(affliction); } + return 1 - ((1 - resistance) * Character.GetAbilityResistance(affliction)); + } - return resistance; + public float GetStatValue(StatTypes statType) + { + float value = 0f; + for (int i = 0; i < afflictions.Count; i++) + { + value += afflictions[i].GetStatValue(statType); + } + return value; + } + + public bool HasFlag(AbilityFlags flagType) + { + for (int i = 0; i < afflictions.Count; i++) + { + if (afflictions[i].HasFlag(flagType)) { return true; } + } + return false; } private readonly List matchingAfflictions = new List(); - public void ReduceAffliction(Limb targetLimb, string affliction, float amount) + public void ReduceAffliction(Limb targetLimb, string affliction, float amount, ActionType? treatmentAction = null) { matchingAfflictions.Clear(); matchingAfflictions.AddRange(afflictions); @@ -486,6 +528,14 @@ namespace Barotrauma for (int i = matchingAfflictions.Count - 1; i >= 0; i--) { var matchingAffliction = matchingAfflictions[i]; + + // this logic runs very often, so culling unnecessary object creation and talent checking with this method + if (Character.HasTalents()) + { + var afflictionReduction = new AbilityValueAffliction(reduceAmount, matchingAffliction); + Character.CheckTalents(AbilityEffectType.OnReduceAffliction, afflictionReduction); + } + if (matchingAffliction.Strength < reduceAmount) { float surplus = reduceAmount - matchingAffliction.Strength; @@ -500,6 +550,17 @@ namespace Barotrauma { matchingAffliction.Strength -= reduceAmount; amount -= reduceAmount; + if (treatmentAction != null) + { + if (treatmentAction.Value == ActionType.OnUse) + { + matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + } + else if (treatmentAction.Value == ActionType.OnFailure) + { + matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + } + } } } CalculateVitality(); @@ -562,9 +623,9 @@ namespace Barotrauma else { // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value. - // Therefore with e.g. 80 health, the max damage per limb would be 20. - // Having at least 20 damage on both legs would cause maximum limping. - float max = MaxVitality / 4; + // Therefore with e.g. 80 health, the max damage per limb would be 40. + // Having at least 40 damage on both legs would cause maximum limping. + float max = MaxVitality / 2; if (string.IsNullOrEmpty(afflictionType)) { float damage = GetAfflictionStrength("damage", limb, true); @@ -595,6 +656,22 @@ namespace Barotrauma CalculateVitality(); } + public void RemoveNegativeAfflictions() + { + // also don't remove genetic effects, even if they're negative + foreach (LimbHealth limbHealth in limbHealths) + { + limbHealth.Afflictions.RemoveAll(a => !a.Prefab.IsBuff && a.Prefab.AfflictionType != "geneticmaterialbuff" && a.Prefab.AfflictionType != "geneticmaterialdebuff"); + } + + afflictions.RemoveAll(a => !irremovableAfflictions.Contains(a) && !a.Prefab.IsBuff && a.Prefab.AfflictionType != "geneticmaterialbuff" && a.Prefab.AfflictionType != "geneticmaterialdebuff"); + foreach (Affliction affliction in irremovableAfflictions) + { + affliction.Strength = 0.0f; + } + CalculateVitality(); + } + private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true) { if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; } @@ -616,7 +693,7 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); if (allowStacking) { // Add the existing strength @@ -638,7 +715,7 @@ namespace Barotrauma //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect var copyAffliction = newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))), + Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); limbHealth.Afflictions.Add(copyAffliction); @@ -660,6 +737,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction, bool allowStacking = true) { if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { @@ -672,7 +750,7 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); if (allowStacking) { // Add the existing strength @@ -694,7 +772,7 @@ namespace Barotrauma //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect afflictions.Add(newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))), + Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), source: newAffliction.Source)); Character.HealthUpdateInterval = 0.0f; @@ -706,8 +784,6 @@ namespace Barotrauma } } - partial void UpdateProjSpecific(float deltaTime); - partial void UpdateLimbAfflictionOverlays(); public void Update(float deltaTime) @@ -716,6 +792,8 @@ namespace Barotrauma StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; + if (Character.GodMode) { return; } + for (int i = 0; i < limbHealths.Count; i++) { for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) @@ -743,11 +821,11 @@ namespace Barotrauma Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } } - + for (int i = afflictions.Count - 1; i >= 0; i--) { var affliction = afflictions[i]; - if (irremovableAfflictions.Contains(affliction)) continue; + if (irremovableAfflictions.Contains(affliction)) { continue; } if (affliction.Strength <= 0.0f) { SteamAchievementManager.OnAfflictionRemoved(affliction, Character); @@ -761,9 +839,21 @@ namespace Barotrauma affliction.DamagePerSecondTimer += deltaTime; Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } - - UpdateLimbAfflictionOverlays(); + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); + + // maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T + if (Character.InWater) + { + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); + } + else + { + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed)); + } + + UpdateLimbAfflictionOverlays(); + UpdateSkinTint(); CalculateVitality(); if (Vitality <= MinVitality) @@ -772,6 +862,32 @@ namespace Barotrauma } } + private void UpdateSkinTint() + { + FaceTint = DefaultFaceTint; + BodyTint = Color.TransparentBlack; + + for (int i = 0; i < limbHealths.Count; i++) + { + for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) + { + var affliction = limbHealths[i].Afflictions[j]; + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } + } + } + for (int i = 0; i < afflictions.Count; i++) + { + var affliction = afflictions[i]; + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } + } + } + private void UpdateOxygen(float deltaTime) { if (!Character.NeedsOxygen) { return; } @@ -784,7 +900,12 @@ namespace Barotrauma } else { - OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f); + float decreaseSpeed = -5.0f; + float increaseSpeed = 10.0f; + float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); + decreaseSpeed *= (1f - oxygenlowResistance); + increaseSpeed *= (1f + oxygenlowResistance); + OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); } UpdateOxygenProjSpecific(prevOxygen, deltaTime); @@ -805,8 +926,6 @@ namespace Barotrauma Vitality = MaxVitality; if (Unkillable || Character.GodMode) { return; } - float damageResistanceMultiplier = 1f - GetResistance("damage"); - foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction affliction in limbHealth.Afflictions) @@ -822,7 +941,6 @@ namespace Barotrauma { vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type]; } - vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } @@ -831,7 +949,6 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { float vitalityDecrease = affliction.GetVitalityDecrease(this); - vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } @@ -848,8 +965,9 @@ namespace Barotrauma { if (Unkillable || Character.GodMode) { return; } - var causeOfDeath = GetCauseOfDeath(); - Character.Kill(causeOfDeath.First, causeOfDeath.Second); + var (type, affliction) = GetCauseOfDeath(); + UpdateSkinTint(); + Character.Kill(type, affliction); #if CLIENT DisplayVitalityDelay = 0.0f; DisplayedVitality = Vitality; @@ -882,7 +1000,7 @@ namespace Barotrauma } } - public Pair GetCauseOfDeath() + public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath() { List currentAfflictions = GetAllAfflictions(true); @@ -903,7 +1021,7 @@ namespace Barotrauma causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation; } - return new Pair(causeOfDeath, strongestAffliction); + return (causeOfDeath, strongestAffliction); } // TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time! @@ -949,15 +1067,16 @@ namespace Barotrauma /// 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. /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, float randomization = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float randomization = 0.0f) { //key = item identifier //float = suitability treatmentSuitability.Clear(); float minSuitability = -10, maxSuitability = 10; - foreach (Affliction affliction in GetAllAfflictions()) + foreach (Affliction affliction in getAfflictions(limb)) { - if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (affliction.Strength <= affliction.Prefab.TreatmentThreshold) { continue; } + if (ignoreHiddenAfflictions && affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { if (!treatmentSuitability.ContainsKey(treatment.Key)) @@ -988,10 +1107,22 @@ namespace Barotrauma treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization; } } + + IEnumerable getAfflictions(Limb limb) + { + if (limb == null) + { + return GetAllAfflictions(); + } + else + { + return GetMatchingAfflictions(GetMatchingLimbHealth(limb)); + } + } } private readonly List activeAfflictions = new List(); - private readonly List> limbAfflictions = new List>(); + private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>(); public void ServerWrite(IWriteMessage msg) { activeAfflictions.Clear(); @@ -1022,22 +1153,22 @@ namespace Barotrauma foreach (Affliction limbAffliction in limbHealth.Afflictions) { if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) continue; - limbAfflictions.Add(new Pair(limbHealth, limbAffliction)); + limbAfflictions.Add((limbHealth, limbAffliction)); } } msg.Write((byte)limbAfflictions.Count); - foreach (var limbAffliction in limbAfflictions) + foreach (var (limbHealth, affliction) in limbAfflictions) { - msg.WriteRangedInteger(limbHealths.IndexOf(limbAffliction.First), 0, limbHealths.Count - 1); - msg.Write(limbAffliction.Second.Prefab.UIntIdentifier); + msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1); + msg.Write(affliction.Prefab.UIntIdentifier); msg.WriteRangedSingle( - MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength), - 0.0f, limbAffliction.Second.Prefab.MaxStrength, 8); - msg.Write((byte)limbAffliction.Second.Prefab.PeriodicEffects.Count()); - foreach (AfflictionPrefab.PeriodicEffect periodicEffect in limbAffliction.Second.Prefab.PeriodicEffects) + MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), + 0.0f, affliction.Prefab.MaxStrength, 8); + msg.Write((byte)affliction.Prefab.PeriodicEffects.Count()); + foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects) { - msg.WriteRangedSingle(limbAffliction.Second.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); + msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8); } } } @@ -1053,7 +1184,7 @@ namespace Barotrauma /// Automatically filters out buffs. /// public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions, bool excludeBuffs = true) => - afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); + afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength); public void Save(XElement healthElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index f87c976ed..e1dfcebc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -217,10 +217,6 @@ namespace Barotrauma if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") { item.AddTag("name:" + character.Name); - if (Level.Loaded != null) - { - item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); - } var job = character.Info?.Job; if (job != null) { @@ -229,6 +225,10 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); idCardComponent?.Initialize(character.Info); + if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) + { + idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; + } var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); foreach (string tag in idCardTags) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index d8cd67162..2841d305e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -89,11 +89,11 @@ namespace Barotrauma return (skill == null) ? 0.0f : skill.Level; } - public void IncreaseSkillLevel(string skillIdentifier, float increase) + public void IncreaseSkillLevel(string skillIdentifier, float increase, bool increasePastMax) { if (skills.TryGetValue(skillIdentifier, out Skill skill)) { - skill.Level += increase; + skill.IncreaseSkill(increase, increasePastMax); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index c918b7363..5bb3e5286 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -68,9 +68,20 @@ namespace Barotrauma } } + public class PreviewItem + { + public readonly string ItemIdentifier; + public readonly bool ShowPreview; + + public PreviewItem(string itemIdentifier, bool showPreview) + { + ItemIdentifier = itemIdentifier; + ShowPreview = showPreview; + } + } + public readonly Dictionary ItemSets = new Dictionary(); - public readonly Dictionary> ItemIdentifiers = new Dictionary>(); - public readonly Dictionary> ShowItemPreview = new Dictionary>(); + public readonly Dictionary> PreviewItems = new Dictionary>(); public readonly List Skills = new List(); public readonly List AutonomousObjectives = new List(); public readonly List AppropriateOrders = new List(); @@ -220,8 +231,7 @@ namespace Barotrauma { case "itemset": ItemSets.Add(variant, subElement); - ItemIdentifiers[variant] = new List(); - ShowItemPreview[variant] = new Dictionary(); + PreviewItems[variant] = new List(); loadItemIdentifiers(subElement, variant); variant++; break; @@ -264,8 +274,7 @@ namespace Barotrauma } else { - ItemIdentifiers[variant].Add(itemIdentifier); - ShowItemPreview[variant][itemIdentifier] = itemElement.GetAttributeBool("showpreview", true); + PreviewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); } loadItemIdentifiers(itemElement, variant); } @@ -275,7 +284,8 @@ namespace Barotrauma Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X)); - ClothingElement = element.GetChildElement("PortraitClothing"); + // Disabled on purpose, TODO: remove all references? + //ClothingElement = element.GetChildElement("PortraitClothing"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index f439cefff..f3a502403 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -7,11 +7,18 @@ namespace Barotrauma private float level; public string Identifier { get; } + + public const float MaximumSkill = 100.0f; public float Level { get { return level; } - set { level = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { level = value; } + } + + public void IncreaseSkill(float value, bool increasePastMax) + { + level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? float.MaxValue : MaximumSkill); } private Sprite icon; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 92087ad79..9731b08d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -11,6 +11,7 @@ using System.Xml.Linq; using Barotrauma.Networking; using LimbParams = Barotrauma.RagdollParams.LimbParams; using JointParams = Barotrauma.RagdollParams.JointParams; +using Barotrauma.Abilities; namespace Barotrauma { @@ -217,9 +218,9 @@ namespace Barotrauma public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; - public bool inWater; + public bool InWater { get; set; } - private readonly FixedMouseJoint pullJoint; + private FixedMouseJoint pullJoint; public readonly LimbType type; @@ -534,14 +535,19 @@ namespace Barotrauma public string Name => Params.Name; - // Exposed for status effects + // These properties are exposed for status effects public bool IsDead => character.IsDead; + public float Health => character.Health; + public float HealthPercentage => character.HealthPercentage; + public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle; public bool CanBeSeveredAlive { get { if (character.IsHumanoid) { return false; } + // TODO: We might need this or solve the cases where a limb is severed while holding on to an item + //if (character.Params.CanInteract) { return false; } if (this == character.AnimController.MainLimb) { return false; } if (character.AnimController.CanWalk) { @@ -683,7 +689,7 @@ namespace Barotrauma private readonly List appliedDamageModifiers = new List(); private readonly List tempModifiers = new List(); private readonly List afflictionsCopy = new List(); - public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f) + public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null) { appliedDamageModifiers.Clear(); afflictionsCopy.Clear(); @@ -741,7 +747,11 @@ namespace Barotrauma { newAffliction.SetStrength(affliction.NonClampedStrength); } - + if (attacker != null) + { + var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character); + attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction); + } if (applyAffliction) { afflictionsCopy.Add(newAffliction); @@ -749,6 +759,10 @@ namespace Barotrauma appliedDamageModifiers.AddRange(tempModifiers); } var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers); + if (result.Afflictions.None()) + { + playSound = false; + } AddDamageProjSpecific(playSound, result); float bleedingDamage = 0; @@ -797,7 +811,7 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime); - if (inWater) + if (InWater) { body.ApplyWaterForces(); } @@ -840,20 +854,25 @@ namespace Barotrauma attack?.UpdateCoolDown(deltaTime); } + private bool temporarilyDisabled; private float reEnableTimer = -1; - public void HideAndDisable(float duration = 0) + public void HideAndDisable(float duration = 0, bool ignoreCollisions = true) { + if (Hidden || Disabled) { return; } + if (ignoreCollisions && IgnoreCollisions) { return; } + temporarilyDisabled = true; Hidden = true; Disabled = true; - IgnoreCollisions = true; + IgnoreCollisions = ignoreCollisions; if (duration > 0) { reEnableTimer = duration; } } - private void ReEnable() + public void ReEnable() { + if (!temporarilyDisabled) { return; } Hidden = false; Disabled = false; IgnoreCollisions = false; @@ -868,7 +887,7 @@ namespace Barotrauma /// public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { - attackResult = default(AttackResult); + attackResult = default; Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; @@ -971,7 +990,7 @@ namespace Barotrauma wasHit = damageTarget != null; } - if (wasHit) + if (wasHit || attack.HitDetectionType == HitDetection.None) { if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -1132,10 +1151,7 @@ namespace Barotrauma if (statusEffect.type != actionType) { continue; } if (statusEffect.type == ActionType.OnDamaged) { - if (statusEffect.AllowedAfflictions != null && (character.LastDamage.Afflictions == null || character.LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) - { - continue; - } + if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } if (statusEffect.OnlyPlayerTriggered) { if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) @@ -1263,6 +1279,14 @@ namespace Barotrauma { body?.Remove(); body = null; + if (pullJoint != null) + { + if (GameMain.World.JointList.Contains(pullJoint)) + { + GameMain.World.Remove(pullJoint); + } + pullJoint = null; + } Release(); RemoveProjSpecific(); Removed = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 649d9afcd..ff46272dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -11,11 +11,12 @@ namespace Barotrauma { public enum AnimationType { - NotDefined, - Walk, - Run, - SwimSlow, - SwimFast + NotDefined = 0, + Walk = 1, + Run = 2, + Crouch = 3, + SwimSlow = 4, + SwimFast = 5 } abstract class GroundedMovementParams : AnimationParams @@ -56,12 +57,15 @@ namespace Barotrauma { [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } + + [Serialize(25.0f, true, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float LegTorque { get; set; } } abstract class AnimationParams : EditableParams, IMemorizable { public string SpeciesName { get; private set; } - public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run; + public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; protected static Dictionary> allAnimations = new Dictionary>(); @@ -110,11 +114,27 @@ namespace Barotrauma } } } + public float TorsoAngleInRadians { get; private set; } = float.NaN; + [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float HeadTorque { get; set; } + + [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float TorsoTorque { get; set; } + + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float FootTorque { get; set; } + [Serialize(AnimationType.NotDefined, true), Editable] public virtual AnimationType AnimationType { get; protected set; } + [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float ArmIKStrength { get; set; } + + [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float HandIKStrength { get; set; } + public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}"; public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); @@ -402,6 +422,8 @@ namespace Barotrauma return typeof(HumanWalkParams); case AnimationType.Run: return typeof(HumanRunParams); + case AnimationType.Crouch: + return typeof(HumanCrouchParams); case AnimationType.SwimSlow: return typeof(HumanSwimSlowParams); case AnimationType.SwimFast: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 07c3bf980..c2257379b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -87,18 +87,9 @@ namespace Barotrauma [Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveForce { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float HeadTorque { get; set; } - - [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float TorsoTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float FootTorque { get; set; } - [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float LegTorque { get; set; } @@ -173,20 +164,12 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] - public float TorsoTorque { get; set; } - - [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] - public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] public float TailTorqueMultiplier { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float FootTorque { get; set; } [Serialize(null, true), Editable] public string FootAngles @@ -224,10 +207,7 @@ namespace Barotrauma Dictionary FootAnglesInRadians { get; set; } float TailAngle { get; set; } float TailAngleInRadians { get; } - float HeadTorque { get; set; } - float TorsoTorque { get; set; } float TailTorque { get; set; } - float FootTorque { get; set; } bool Flip { get; set; } float FlipCooldown { get; set; } float FlipDelay { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 639b7f46e..4192bd331 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -24,6 +24,17 @@ namespace Barotrauma public override void StoreSnapshot() => StoreSnapshot(); } + class HumanCrouchParams : HumanGroundedParams + { + public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); + public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) + { + return GetAnimParams(character.SpeciesName, AnimationType.Crouch, fileName); + } + + public override void StoreSnapshot() => StoreSnapshot(); + } + class HumanSwimFastParams: HumanSwimParams { public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); @@ -58,9 +69,6 @@ namespace Barotrauma [Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize(0.5f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float HandMoveStrength { get; set; } - [Serialize(5.0f, true), Editable] public float HandCycleSpeed { get; set; } @@ -81,36 +89,17 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] - public float FootRotateStrength { get; set; } + [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] + public float ArmMoveStrength { get; set; } + + [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float HandMoveStrength { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation { [Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } - - // -- TODO: use a separate clip for crawling -> replace these when implemented. - - [Serialize(0.65f, true, description: "Height of the torso when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] - public float CrouchingTorsoPos { get; set; } - - [Serialize(0.65f, true, description: "Height of the head when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] - public float CrouchingHeadPos { get; set; } - - /// - /// In degrees - /// - [Serialize(-10f, true, description: "Angle of the torso when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] - public float CrouchingTorsoAngle { get; set; } - - /// - /// In degrees - /// - [Serialize(-10f, true, description: "Angle of the head when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] - public float CrouchingHeadAngle { get; set; } - - // -- [Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] public float HeadLeanAmount { get; set; } @@ -121,6 +110,9 @@ namespace Barotrauma [Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } + [Serialize(0f, true, 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; } + /// /// In degrees. /// @@ -135,15 +127,9 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(20.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] - public float FootRotateStrength { get; set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] public Vector2 FootMoveOffset { get; set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] - public Vector2 CrouchingFootMoveOffset { get; set; } - [Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float LegBendTorque { get; set; } @@ -153,17 +139,23 @@ namespace Barotrauma [Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } - [Serialize(0.7f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] - public float HandMoveStrength { get; set; } - [Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] public float HandClampY { get; set; } + + [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float ArmMoveStrength { get; set; } + + [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float HandMoveStrength { get; set; } } public interface IHumanAnimation { float FootAngle { get; set; } float FootAngleInRadians { get; } - float FootRotateStrength { get; set; } + + float ArmMoveStrength { get; set; } + + float HandMoveStrength { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 40e20c196..39016b7d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -34,6 +34,9 @@ namespace Barotrauma [Serialize(false, true), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } + [Serialize(false, true, description: "Can the creature interact with items?"), Editable] + public bool CanInteract { get; private set; } + [Serialize(false, true), Editable] public bool Husk { get; private set; } @@ -70,15 +73,24 @@ namespace Barotrauma [Serialize(1f, true), Editable] public float BleedParticleMultiplier { get; private set; } + [Serialize(true, true, 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] + public bool CanEat { get; set; } + [Serialize(10f, true, 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, true), Editable] + public bool UsePathFinding { get; set; } + [Serialize(1f, true, "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, true), Editable] public bool HideInSonar { get; set; } + [Serialize(false, true), Editable] + public bool HideInThermalGoggles { get; set; } + [Serialize(0f, true), Editable] public float SonarDisruption { get; set; } @@ -88,6 +100,9 @@ namespace Barotrauma [Serialize(25000f, true, "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)] public float DisableDistance { get; set; } + [Serialize(10f, true, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] + public float SoundInterval { get; set; } + public readonly string File; public XDocument VariantFile { get; private set; } @@ -448,6 +463,9 @@ namespace Barotrauma [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } + [Serialize(false, true), Editable] + public bool StunImmunity { get; set; } + // TODO: limbhealths, sprite? public HealthParams(XElement element, CharacterParams character) : base(element, character) { } @@ -547,15 +565,24 @@ namespace Barotrauma [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] public bool RandomAttack { get; private set; } - [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable] + [Serialize(false, true, 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, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] + public bool KeepDoorsClosed { get; private set; } + [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] public bool AvoidAbyss { get; set; } [Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] public bool StayInAbyss { get; set; } + [Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] + public bool PatrolFlooded { get; set; } + + [Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] + public bool PatrolDry { get; set; } + [Serialize(0f, true, description: ""), Editable] public float StartAggression { get; private set; } @@ -676,8 +703,17 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool IgnoreIncapacitated { get; set; } - [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable] - public float DamageThreshold { get; private set; } + [Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + public float Threshold { get; private set; } + + [Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable] + public float ThresholdMin { get; private set; } + + [Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable] + public float ThresholdMax { get; private set; } + + [Serialize("0.0, 0.0", true), Editable] + public Vector2 Offset { get; private set; } [Serialize(AttackPattern.Straight, true), Editable] public AttackPattern AttackPattern { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 9c41dddb0..ff4f961f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -34,7 +34,10 @@ namespace Barotrauma [Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] public string Texture { get; set; } - [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)] + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + public Color Color { get; set; } + + [Serialize(0.0f, true, 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)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -556,7 +559,7 @@ namespace Barotrauma } } - public override string GenerateName() => $"Limb {ID}"; + public override string GenerateName() => Type != LimbType.None ? $"{Type} ({ID})" : $"Limb {ID}"; public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams; @@ -569,12 +572,14 @@ namespace Barotrauma /// /// The orientation of the sprite as drawn on the sprite sheet (in radians). /// - public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation); + public float GetSpriteOrientation() => MathHelper.ToRadians(GetSpriteOrientationInDegrees()); + + public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation; [Serialize("", true), Editable] public string Notes { get; set; } - [Serialize(1f, true), Editable] + [Serialize(1f, true), Editable(DecimalCount = 2)] public float Scale { get; set; } [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] @@ -589,9 +594,12 @@ namespace Barotrauma [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + [Serialize(float.NaN, true, 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)] public float SpriteOrientation { get; set; } + [Serialize(LimbType.None, true, 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).")] + public LimbType InheritLimbDepth { get; set; } + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } @@ -674,6 +682,9 @@ namespace Barotrauma [Serialize(50f, true), Editable] public float BlinkForce { get; set; } + [Serialize(false, true), Editable] + public bool OnlyBlinkInWater { get; set; } + [Serialize(TransitionMode.Linear, true), Editable] public TransitionMode BlinkTransitionIn { get; private set; } @@ -889,6 +900,9 @@ namespace Barotrauma [Serialize("", true), Editable()] public string Texture { get; set; } + [Serialize(false, true), Editable()] + public bool IgnoreTint { get; set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] public Color Color { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs new file mode 100644 index 000000000..959cf4148 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -0,0 +1,92 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityCondition + { + protected CharacterTalent characterTalent; + protected Character character; + protected bool invert; + + public virtual bool AllowClientSimulation => true; + + public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement) + { + this.characterTalent = characterTalent; + character = characterTalent.Character; + invert = conditionElement.GetAttributeBool("invert", false); + } + public abstract bool MatchesCondition(AbilityObject abilityObject); + public abstract bool MatchesCondition(); + + + // tools + protected enum TargetType + { + Any = 0, + Enemy = 1, + Ally = 2, + NotSelf = 3, + Alive = 4, + Monster = 5, + InFriendlySubmarine = 6, + }; + + protected List ParseTargetTypes(string[] targetTypeStrings) + { + List targetTypes = new List(); + foreach (string targetTypeString in targetTypeStrings) + { + TargetType targetType = TargetType.Any; + if (!Enum.TryParse(targetTypeString, true, out targetType)) + { + DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + targetTypes.Add(targetType); + } + return targetTypes; + } + + protected bool IsViableTarget(IEnumerable targetTypes, Character targetCharacter) + { + if (targetCharacter == null) { return false; } + + bool isViable = true; + foreach (TargetType targetType in targetTypes) + { + if (!IsViableTarget(targetType, targetCharacter)) + { + isViable = false; + break; + } + } + return isViable; + } + + private bool IsViableTarget(TargetType targetType, Character targetCharacter) + { + switch (targetType) + { + case TargetType.Enemy: + return !HumanAIController.IsFriendly(character, targetCharacter); + case TargetType.Ally: + return HumanAIController.IsFriendly(character, targetCharacter); + case TargetType.NotSelf: + return targetCharacter != character; + case TargetType.Alive: + return !targetCharacter.IsDead; + case TargetType.Monster: + return !targetCharacter.IsHuman; + case TargetType.InFriendlySubmarine: + return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID; + default: + return true; + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs new file mode 100644 index 000000000..4d909aa81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -0,0 +1,29 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAffliction : AbilityConditionData + { + private readonly string[] afflictions; + public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + { + return afflictions.Any(a => a == affliction.Identifier); + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs new file mode 100644 index 000000000..ee5fedac0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -0,0 +1,79 @@ +using Barotrauma.Items.Components; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAttackData : AbilityConditionData + { + private enum WeaponType + { + Any = 0, + Melee = 1, + Ranged = 2 + }; + + private readonly string itemIdentifier; + private readonly string[] tags; + private readonly WeaponType weapontype; + public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + itemIdentifier = conditionElement.GetAttributeString("itemidentifier", ""); + tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + switch (conditionElement.GetAttributeString("weapontype", "")) + { + case "melee": + weapontype = WeaponType.Melee; + break; + case "ranged": + weapontype = WeaponType.Ranged; + break; + } + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is AbilityAttackData attackData) + { + Item item = attackData?.SourceAttack?.SourceItem; + + if (item == null) + { + DebugConsole.AddWarning($"Source Item was not found in {this} for talent {characterTalent.DebugIdentifier}!"); + return false; + } + + if (!string.IsNullOrEmpty(itemIdentifier)) + { + if (item.prefab.Identifier != itemIdentifier) + { + return false; + } + } + + if (tags.Any()) + { + if (!tags.All(t => item.HasTag(t))) + { + return false; + } + } + + switch (weapontype) + { + case WeaponType.Melee: + return item.GetComponent() != null; + case WeaponType.Ranged: + return item.GetComponent() != null; + } + + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(AbilityAttackData)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs new file mode 100644 index 000000000..58616eac5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -0,0 +1,38 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAttackResult : AbilityConditionData + { + private readonly List targetTypes; + private readonly string[] afflictions; + public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityAttackResult)?.AttackResult is AttackResult attackResult) + { + if (!IsViableTarget(targetTypes, attackResult.HitLimb?.character)) { return false; } + + if (afflictions.Any()) + { + if (attackResult.Afflictions == null || !afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; } + } + + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs new file mode 100644 index 000000000..2670ea21e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCharacter : AbilityConditionData + { + private readonly List targetTypes; + + public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character character) + { + if (!IsViableTarget(targetTypes, character)) { return false; } + + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs new file mode 100644 index 000000000..fe3608df4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -0,0 +1,36 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityConditionData : AbilityCondition + { + /// + /// Some conditions rely on specific ability data that is integrally connected to the AbilityEffectType. + /// This is done in order to avoid having to create duplicate ability behavior, such as if an ability needs to trigger + /// a common ability effect but in specific circumstances. These conditions could also be partially replaced by + /// more explicit AbilityEffectType enums, but this would introduce bloat and overhead to integral game logic + /// when instead said logic can be made to only run when required using these conditions. + /// + /// These conditions will return an error if used outside their limited intended use. + /// + public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) + { + DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject}"); + } + + protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject); + public override bool MatchesCondition() + { + DebugConsole.ThrowError("Used data-reliant ability condition in a state-based ability! This is not allowed."); + return false; + } + public override bool MatchesCondition(AbilityObject abilityObject) + { + if (abilityObject is null) { return invert; } + return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs new file mode 100644 index 000000000..2e1204fba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs @@ -0,0 +1,22 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionEvasiveManeuvers : AbilityConditionData + { + public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilitySubmarine)?.Submarine is Submarine submarine && (abilityObject as IAbilityCharacter)?.Character is Character attackingCharacter) + { + return submarine.TeamID == character.TeamID && character.Submarine == submarine && attackingCharacter.TeamID != character.TeamID; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilitySubmarine)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs new file mode 100644 index 000000000..26a04a1a7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs @@ -0,0 +1,60 @@ +using Barotrauma.Items.Components; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionIsAiming : AbilityConditionDataless + { + private enum WeaponType + { + Any = 0, + Melee = 1, + Ranged = 2 + }; + + private readonly bool hittingCountsAsAiming; + + private readonly WeaponType weapontype; + public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + hittingCountsAsAiming = conditionElement.GetAttributeBool("hittingcountsasaiming", false); + switch (conditionElement.GetAttributeString("weapontype", "")) + { + case "melee": + weapontype = WeaponType.Melee; + break; + case "ranged": + weapontype = WeaponType.Ranged; + break; + } + } + + protected override bool MatchesConditionSpecific() + { + if (character.AnimController is HumanoidAnimController animController) + { + foreach (Item item in character.HeldItems) + { + switch (weapontype) + { + case WeaponType.Melee: + var meleeWeapon = item.GetComponent(); + if (meleeWeapon != null) + { + if (animController.IsAimingMelee || (meleeWeapon.Hitting && hittingCountsAsAiming)) { return true; } + } + break; + case WeaponType.Ranged: + if (animController.IsAiming && item.GetComponent() != null) { return true; } + break; + default: + if (animController.IsAiming || animController.IsAimingMelee) { return true; } + break; + } + } + } + + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs new file mode 100644 index 000000000..d4eb985d2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItem : AbilityConditionData + { + private readonly string[] identifiers; + private readonly string[] tags; + + public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty(), convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + ItemPrefab itemPrefab = null; + if ((abilityObject as IAbilityItemPrefab)?.ItemPrefab is ItemPrefab abilityItemPrefab) + { + itemPrefab = abilityItemPrefab; + } + else if ((abilityObject as IAbilityItem)?.Item is Item abilityItem) + { + itemPrefab = abilityItem.Prefab; + } + + if (itemPrefab != null) + { + if (identifiers.Any()) + { + if (!identifiers.Any(t => itemPrefab.Identifier == t)) + { + return false; + } + } + + return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p)); + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs new file mode 100644 index 000000000..d23794f56 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs @@ -0,0 +1,23 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemOutsideSubmarine : AbilityConditionData + { + + public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs new file mode 100644 index 000000000..81d1b1d06 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs @@ -0,0 +1,23 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemWreck : AbilityConditionData + { + + public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + return item.Submarine?.Info?.IsWreck ?? false; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs new file mode 100644 index 000000000..b2d70b0b3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionLocation : AbilityConditionData + { + private readonly bool? hasOutpost; + private readonly string[] locationIdentifiers; + + public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + if (conditionElement.Attribute("hasoutpost") != null) + { + hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); + } + locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is IAbilityLocation abilityLocation) + { + if (locationIdentifiers.Any()) + { + if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; } + } + if (hasOutpost.HasValue) + { + if (hasOutpost.Value != abilityLocation.Location.HasOutpost()) { return false; } + } + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs new file mode 100644 index 000000000..f7f0ffed4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionMission : AbilityConditionData + { + private readonly MissionType missionType; + public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); + if (!Enum.TryParse(missionTypeString, out missionType)) + { + DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type."); + return; + } + if (missionType == MissionType.None) + { + DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none."); + return; + } + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityMission)?.Mission is Mission mission) + { + return mission.Prefab.Type == missionType; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs new file mode 100644 index 000000000..1068da08b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -0,0 +1,33 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionReduceAffliction : AbilityConditionData + { + private readonly string[] allowedTypes; + private readonly string identifier; + + public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true); + identifier = conditionElement.GetAttributeString("identifier", ""); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + { + if (allowedTypes.Find(c => c == affliction.Prefab.AfflictionType) == null) { return false; } + + if (!string.IsNullOrEmpty(identifier) && affliction.Prefab.Identifier != identifier) { return false; } + + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityAffliction)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs new file mode 100644 index 000000000..5c368df8f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -0,0 +1,32 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionSkill : AbilityConditionData + { + private readonly string skillIdentifier; + + public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + } + + private bool MatchesConditionSpecific(string skillIdentifier) + { + return this.skillIdentifier == skillIdentifier; + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + { + return MatchesConditionSpecific(skillIdentifier); + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityString)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs new file mode 100644 index 000000000..9fe8fa1a4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAboveVitality : AbilityConditionDataless + { + private readonly float vitalityPercentage; + + public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.HealthPercentage / 100f > vitalityPercentage; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs new file mode 100644 index 000000000..29256ab7c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs @@ -0,0 +1,19 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAlliesAboveVitality : AbilityConditionDataless + { + float vitalityPercentage; + + public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); + } + protected override bool MatchesConditionSpecific() + { + return Character.GetFriendlyCrew(character).All(c => c.HealthPercentage / 100f >= vitalityPercentage); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs new file mode 100644 index 000000000..7525427eb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCoauthor : AbilityConditionDataless + { + private readonly string jobIdentifier; + + public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty); + } + + protected override bool MatchesConditionSpecific() + { + if (character.SelectedCharacter is Character otherCharacter) + { + if (!otherCharacter.HasJob(jobIdentifier)) { return false; } + if (!(character.SelectedBy == otherCharacter)) { return false; } + return true; + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs new file mode 100644 index 000000000..cd96edb58 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCrouched : AbilityConditionDataless + { + + public AbilityConditionCrouched(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return character.AnimController is HumanoidAnimController humanoidAnimController && humanoidAnimController.Crouching; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs new file mode 100644 index 000000000..ad7007fd6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -0,0 +1,20 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class AbilityConditionDataless : AbilityCondition + { + public AbilityConditionDataless(CharacterTalent characterTalent, XElement conditionElement) : base (characterTalent, conditionElement) { } + + protected abstract bool MatchesConditionSpecific(); + public override bool MatchesCondition() + { + return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); + } + + public override bool MatchesCondition(AbilityObject abilityObject) + { + return invert ? !MatchesConditionSpecific() : MatchesConditionSpecific(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs new file mode 100644 index 000000000..9f449e43c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasAffliction : AbilityConditionDataless + { + private string afflictionIdentifier; + private float minimumPercentage; + + + public AbilityConditionHasAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", ""); + minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + if (!string.IsNullOrEmpty(afflictionIdentifier)) + { + var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier); + + if (affliction == null) { return false; } + + return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength; + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs new file mode 100644 index 000000000..0f1707d3d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasDifferentJobs : AbilityConditionDataless + { + private readonly int amount; + public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + amount = conditionElement.GetAttributeInt("amount", 0); + } + + protected override bool MatchesConditionSpecific() + { + IEnumerable crewmembers = Character.GetFriendlyCrew(character); + int differentCrewAmount = crewmembers.Select(c => c.Info?.Job?.Prefab.Identifier).Distinct().Count(); + return differentCrewAmount >= amount; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs new file mode 100644 index 000000000..8f4fc7c35 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -0,0 +1,57 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasItem : AbilityConditionDataless + { + // not used for anything atm, will be used for clown subclass + private readonly string[] tags; + private InvSlotType? invSlotType; + bool requireAll; + + private List items = new List(); + + public AbilityConditionHasItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + requireAll = conditionElement.GetAttributeBool("requireall", false); + //this.invSlotType = invSlotType; + } + + protected override bool MatchesConditionSpecific() + { + items.Clear(); + if (tags.Any()) + { + foreach (string tag in tags) + { + // there is a better method, should use that instead + if (character.GetEquippedItem(tag, invSlotType) is Item foundItem) + { + items.Add(foundItem); + } + } + + } + else + { + if (character.GetEquippedItem(null, invSlotType) is Item foundItem) + { + items.Add(foundItem); + } + } + + if (requireAll) + { + return (items.Count >= tags.Count()); + } + else + { + return items.Any(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs new file mode 100644 index 000000000..2c3b26a5c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasPermanentStat : AbilityConditionDataless + { + private readonly string statIdentifier; + private readonly StatTypes statType; + private readonly float min; + + public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty); + if (string.IsNullOrEmpty(statIdentifier)) + { + DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); + } + string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); + statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); + min = conditionElement.GetAttributeFloat("min", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.Info.GetSavedStatValue(statType, statIdentifier) >= min; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs new file mode 100644 index 000000000..60d5da1f7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs @@ -0,0 +1,24 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasSkill : AbilityConditionDataless + { + private readonly string skillIdentifier; + private readonly float minValue; + + public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty); + minValue = conditionElement.GetAttributeFloat("minvalue", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.GetSkillLevel(skillIdentifier) >= minValue; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs new file mode 100644 index 000000000..2a22f2098 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasStatusTag : AbilityConditionDataless + { + private readonly string tag; + + + public AbilityConditionHasStatusTag(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + tag = conditionElement.GetAttributeString("tag", ""); + if (string.IsNullOrEmpty(tag)) + { + DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); + } + } + + protected override bool MatchesConditionSpecific() + { + if (!string.IsNullOrEmpty(tag)) + { + return + StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) || + DelayedEffect.DelayList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)); + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs new file mode 100644 index 000000000..d3aa75bde --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasVelocity : AbilityConditionDataless + { + private readonly float velocity; + + public AbilityConditionHasVelocity(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + velocity = conditionElement.GetAttributeFloat("velocity", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.AnimController.Collider.LinearVelocity.LengthSquared() > velocity * velocity; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs new file mode 100644 index 000000000..28f27ed9b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInFriendlySubmarine : AbilityConditionDataless + { + public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.Submarine?.TeamID == character.TeamID; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs new file mode 100644 index 000000000..e08291e6b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInHull : AbilityConditionDataless + { + public AbilityConditionInHull(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.CurrentHull != null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs new file mode 100644 index 000000000..d93731514 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInWater : AbilityConditionDataless + { + public AbilityConditionInWater(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.InWater; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs new file mode 100644 index 000000000..f2c4b2fb7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionLevelsBehindHighest : AbilityConditionDataless + { + private readonly int levelsBehind; + public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + levelsBehind = conditionElement.GetAttributeInt("levelsbehind", 0); + } + + protected override bool MatchesConditionSpecific() + { + return Character.GetFriendlyCrew(character).Where(c => c.Info != null && (c.Info.GetCurrentLevel() - character.Info.GetCurrentLevel() >= levelsBehind)).Any(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs new file mode 100644 index 000000000..bb4390106 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionNoCrewDied : AbilityConditionDataless + { + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + return !campaign.CrewHasDied; + } + return true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs new file mode 100644 index 000000000..dac9a3f1a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionOnMission : AbilityConditionDataless + { + public AbilityConditionOnMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return Level.Loaded?.Type != LevelData.LevelType.Outpost; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs new file mode 100644 index 000000000..192ea6f4f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -0,0 +1,18 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionRagdolled : AbilityConditionDataless + { + + public AbilityConditionRagdolled(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + } + + protected override bool MatchesConditionSpecific() + { + return character.IsRagdolled || character.Stun > 0f || character.IsIncapacitated; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs new file mode 100644 index 000000000..3186b852f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionRunning : AbilityConditionDataless + { + public AbilityConditionRunning(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.AnimController is HumanoidAnimController animController && animController.IsMovingFast; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs new file mode 100644 index 000000000..5b582f799 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs @@ -0,0 +1,20 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionServerRandom : AbilityConditionDataless + { + private readonly float randomChance = 0f; + public override bool AllowClientSimulation => false; + + public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + randomChance = conditionElement.GetAttributeFloat("randomchance", 1f); + } + + protected override bool MatchesConditionSpecific() + { + return randomChance >= Rand.Range(0f, 1f, Rand.RandSync.Unsynced); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs new file mode 100644 index 000000000..9a99f4ce8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionShipFlooded : AbilityConditionDataless + { + private readonly float floodPercentage; + public AbilityConditionShipFlooded(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + floodPercentage = conditionElement.GetAttributeFloat("floodpercentage", 0f); + } + + protected override bool MatchesConditionSpecific() + { + if (!character.IsInFriendlySub) { return false; } + float currentFloodPercentage = character.Submarine.GetHulls(false).Average(h => h.WaterPercentage); + return currentFloodPercentage / 100 > floodPercentage; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs new file mode 100644 index 000000000..8c552ad82 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -0,0 +1,52 @@ +namespace Barotrauma.Abilities +{ + interface IAbilityItemPrefab + { + public ItemPrefab ItemPrefab { get; set; } + } + + interface IAbilityItem + { + public Item Item { get; set; } + } + + interface IAbilityValue + { + public float Value { get; set; } + } + + interface IAbilityMission + { + public Mission Mission { get; set; } + } + + interface IAbilityLocation + { + public Location Location { get; set; } + } + + interface IAbilityCharacter + { + public Character Character { get; set; } + } + + interface IAbilityString + { + public string String { get; set; } + } + + interface IAbilityAffliction + { + public Affliction Affliction { get; set; } + } + + interface IAbilityAttackResult + { + public AttackResult AttackResult { get; set; } + } + + interface IAbilitySubmarine + { + public Submarine Submarine { get; set; } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs new file mode 100644 index 000000000..49431bb1e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; + +namespace Barotrauma.Abilities +{ + abstract class AbilityObject + { + // kept as blank for now, as we are using a composition and only using this object to enforce parameter types + } + + class AbilityCharacter : AbilityObject, IAbilityCharacter + { + public AbilityCharacter(Character character) + { + Character = character; + } + public Character Character { get; set; } + } + + class AbilityItem : AbilityObject, IAbilityItem + { + public AbilityItem(Item item) + { + Item = item; + } + public Item Item { get; set; } + } + + class AbilityValue : AbilityObject, IAbilityValue + { + public AbilityValue(float value) + { + Value = value; + } + public float Value { get; set; } + } + + class AbilityAffliction : AbilityObject, IAbilityAffliction + { + public AbilityAffliction(Affliction affliction) + { + Affliction = affliction; + } + public Affliction Affliction { get; set; } + } + + class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter + { + public AbilityAfflictionCharacter(Affliction affliction, Character character) + { + Affliction = affliction; + Character = character; + } + public Character Character { get; set; } + public Affliction Affliction { get; set; } + } + + class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityValueItem(float value, ItemPrefab itemPrefab) + { + Value = value; + ItemPrefab = itemPrefab; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } + + class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab + { + public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab) + { + Item = item; + ItemPrefab = itemPrefab; + } + public Item Item { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } + + class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString + { + public AbilityValueString(float value, string abilityString) + { + Value = value; + String = abilityString; + } + public float Value { get; set; } + public string String { get; set; } + } + + class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString + { + public AbilityStringCharacter(string abilityString, Character character) + { + String = abilityString; + Character = character; + } + public Character Character { get; set; } + public string String { get; set; } + } + + class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction + { + public AbilityValueAffliction(float value, Affliction affliction) + { + Value = value; + Affliction = affliction; + } + public float Value { get; set; } + public Affliction Affliction { get; set; } + } + + class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityValueMission(float value, Mission mission) + { + Value = value; + Mission = mission; + } + public float Value { get; set; } + public Mission Mission { get; set; } + } + + class AbilityLocation : AbilityObject, IAbilityLocation + { + public AbilityLocation(Location location) + { + Location = location; + } + + public Location Location { get; set; } + } + + // this is an exception class that should only be passed in this form, so classes that use it should cast into it directly + class AbilityAttackData : AbilityObject, IAbilityCharacter + { + public float DamageMultiplier { get; set; } = 1f; + public float AddedPenetration { get; set; } = 0f; + public List Afflictions { get; set; } + public Attack SourceAttack { get; } + public Character Character { get; set; } + public Character Attacker { get; set; } + + public AbilityAttackData(Attack sourceAttack, Character character) + { + SourceAttack = sourceAttack; + Character = character; + } + } + + class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem + { + public Character Character { get; set; } + + public Character User { get; set; } + + public Item Item { get; set; } + + public AbilityApplyTreatment(Character user, Character target, Item item) + { + Character = target; + User = user; + Item = item; + } + } + + class AbilityAttackResult : AbilityObject, IAbilityAttackResult + { + public AttackResult AttackResult { get; set; } + + public AbilityAttackResult(AttackResult attackResult) + { + AttackResult = attackResult; + } + } + + class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine + { + public AbilityCharacterSubmarine(Character character, Submarine submarine) + { + Character = character; + Submarine = submarine; + } + public Character Character { get; set; } + public Submarine Submarine { get; set; } + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs new file mode 100644 index 000000000..650bf1863 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class CharacterAbility + { + public CharacterAbilityGroup CharacterAbilityGroup { get; } + public CharacterTalent CharacterTalent { get; } + public Character Character { get; } + + public bool RequiresAlive { get; } + + public virtual bool AllowClientSimulation => false; + public virtual bool AppliesEffectOnIntervalUpdate => false; + + private const float DefaultEffectTime = 1.0f; + + // currently resets if the character dies. would need to be stored in a dictionary of sorts to maintain through death + + + /// + /// Used primarily for StatusEffects. Default to constant outside interval abilities. + /// + protected float EffectDeltaTime => CharacterAbilityGroup is CharacterAbilityGroupInterval abilityGroupInterval ? abilityGroupInterval.TimeSinceLastUpdate : DefaultEffectTime; + + public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) + { + CharacterAbilityGroup = characterAbilityGroup; + CharacterTalent = characterAbilityGroup.CharacterTalent; + Character = CharacterTalent.Character; + RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true); + } + + public bool IsViable() + { + if (!AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + if (RequiresAlive && Character.IsDead) { return false; } + return true; + } + + public virtual void InitializeAbility(bool addingFirstTime) { } + + public virtual void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) + { + // may need a separate Update for changing state on non-interval-based abilities + if (AppliesEffectOnIntervalUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + else + { + VerifyState(conditionsMatched, timeSinceLastUpdate); + } + } + + protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); + } + + public void ApplyAbilityEffect(AbilityObject abilityObject) + { + if (abilityObject is null) + { + ApplyEffect(); + } + else + { + ApplyEffect(abilityObject); + } + } + + protected virtual void ApplyEffect() + { + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect"); + } + + protected virtual void ApplyEffect(AbilityObject abilityObject) + { + DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect"); + } + + protected void LogabilityObjectMismatch() + { + DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type."); + } + + // XML + public static CharacterAbility Load(XElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) + { + Type abilityType; + string type = abilityElement.Name.ToString().ToLowerInvariant(); + try + { + abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + if (abilityType == null) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")"); + return null; + } + } + catch (Exception e) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e); + return null; + } + + object[] args = { characterAbilityGroup, abilityElement }; + CharacterAbility characterAbility; + + try + { + characterAbility = (CharacterAbility)Activator.CreateInstance(abilityType, args); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException); + return null; + } + + return characterAbility; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs new file mode 100644 index 000000000..7dde00097 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -0,0 +1,61 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyForce : CharacterAbility + { + private readonly float force; + private readonly float maxVelocity; + + private readonly string afflictionIdentifier; + + private readonly HashSet limbTypes = new HashSet(); + + public override bool AppliesEffectOnIntervalUpdate => true; + public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + force = abilityElement.GetAttributeFloat("force", 0f); + maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f); + afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", ""); + + string[] limbTypesStr = abilityElement.GetAttributeStringArray("limbtypes", new string[0]); + foreach (string limbTypeStr in limbTypesStr) + { + if (Enum.TryParse(limbTypeStr, out LimbType limbType)) + { + limbTypes.Add(limbType); + } + else + { + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type."); + } + } + } + + protected override void ApplyEffect() + { + float strength = 1.0f; + if (!string.IsNullOrEmpty(afflictionIdentifier)) + { + Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier); + if (affliction == null) { return; } + strength = affliction.Strength / affliction.Prefab.MaxStrength; + } + + foreach (Limb limb in Character.AnimController.Limbs) + { + if (limb.IsSevered || limb.Removed) { continue; } + if (limbTypes.Any()) + { + if (!limbTypes.Contains(limb.type)) { continue; } + } + if (Character.AnimController.TargetMovement.LengthSquared() < 0.001f) { continue; } + limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * force * strength, maxVelocity); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs new file mode 100644 index 000000000..a59955d4d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffects : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + public override bool AllowClientSimulation => true; + + protected readonly List statusEffects; + + private readonly bool nearbyCharactersAppliesToSelf; + private readonly bool nearbyCharactersAppliesToAllies; + private readonly bool applyToSelected; + + readonly List targets = new List(); + + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + applyToSelected = abilityElement.GetAttributeBool("applytoselected", false); + nearbyCharactersAppliesToSelf = abilityElement.GetAttributeBool("nearbycharactersappliestoself", true); + nearbyCharactersAppliesToAllies = abilityElement.GetAttributeBool("nearbycharactersappliestoallies", true); + } + + protected void ApplyEffectSpecific(Character targetCharacter) + { + foreach (var statusEffect in statusEffects) + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + // currently used to spawn items on the targeted character + statusEffect.SetUser(targetCharacter); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); + if (!nearbyCharactersAppliesToSelf) + { + targets.RemoveAll(c => c == Character); + } + if (!nearbyCharactersAppliesToAllies) + { + targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + } + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + else + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); + } + } + } + protected override void ApplyEffect() + { + if (applyToSelected && Character.SelectedCharacter is Character selectedCharacter) + { + ApplyEffectSpecific(selectedCharacter); + } + else + { + ApplyEffectSpecific(Character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffect(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs new file mode 100644 index 000000000..9cbaa17eb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToAllies : CharacterAbilityApplyStatusEffects + { + private readonly bool allowSelf; + private readonly float maxDistance = float.MaxValue; + + public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + allowSelf = abilityElement.GetAttributeBool("allowself", true); + maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + } + + + protected override void ApplyEffect() + { + IEnumerable chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character); + + foreach (Character character in chosenCharacters) + { + if (maxDistance < float.MaxValue) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } + } + ApplyEffectSpecific(character); + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs new file mode 100644 index 000000000..efca07622 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects + { + public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as AbilityAttackData)?.Attacker is Character attacker) + { + ApplyEffectSpecific(attacker); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs new file mode 100644 index 000000000..fc4291453 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects + { + public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect() + { + if (IsViableTarget(Character.LastOrderedCharacter)) + { + ApplyEffectSpecific(Character.LastOrderedCharacter); + } + if (Character.HasAbilityFlag(AbilityFlags.AllowSecondOrderedTarget) && IsViableTarget(Character.SecondLastOrderedCharacter)) + { + ApplyEffectSpecific(Character.SecondLastOrderedCharacter); + } + } + + private bool IsViableTarget(Character targetCharacter) + { + if (targetCharacter == null || targetCharacter.Removed) { return false; } + if (targetCharacter == Character) { return false; } + return true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs new file mode 100644 index 000000000..a0701c782 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects + { + protected float squaredMaxDistance; + public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); + } + + protected override void ApplyEffect() + { + Character closestCharacter = null; + float closestDistance = float.MaxValue; + + foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) + { + if (crewCharacter != Character && Vector2.DistanceSquared(Character.WorldPosition, crewCharacter.WorldPosition) is float tempDistance && tempDistance < closestDistance) + { + closestCharacter = crewCharacter; + closestDistance = tempDistance; + } + } + + if (closestDistance < squaredMaxDistance) + { + ApplyEffectSpecific(closestCharacter); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs new file mode 100644 index 000000000..0f1fd20b2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs @@ -0,0 +1,41 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToRandomAlly : CharacterAbilityApplyStatusEffects + { + private readonly float squaredMaxDistance; + private readonly bool allowDifferentSub; + private readonly bool allowSelf; + + public override bool AllowClientSimulation => false; + + public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); + allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true); + allowSelf = abilityElement.GetAttributeBool("allowself", true); + } + + protected override void ApplyEffect() + { + Character chosenCharacter = null; + + chosenCharacter = Character.GetFriendlyCrew(Character).Where(c => + (allowSelf || c != Character) && + (allowDifferentSub || c.Submarine == Character.Submarine) && + Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) is float tempDistance && + tempDistance < squaredMaxDistance).GetRandom(); + + if (chosenCharacter == null) { return; } + + ApplyEffectSpecific(chosenCharacter); + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs new file mode 100644 index 000000000..43fef2a11 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -0,0 +1,27 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGainSimultaneousSkill : CharacterAbility + { + private string skillIdentifier; + + public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityValue)?.Value is float skillIncrease) + { + Character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease); + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs new file mode 100644 index 000000000..5f56b433a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveAffliction : CharacterAbility + { + private readonly string afflictionId; + private readonly float strength; + private readonly string multiplyStrengthBySkill; + private readonly bool setValue; + + public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty)); + strength = abilityElement.GetAttributeFloat("strength", 0f); + multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty); + setValue = abilityElement.GetAttributeBool("setvalue", false); + + if (string.IsNullOrEmpty(afflictionId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is IAbilityCharacter character) + { + var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase)); + if (afflictionPrefab == null) + { + DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); + return; + } + float strength = this.strength; + if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) + { + strength *= Character.GetSkillLevel(multiplyStrengthBySkill); + } + character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs new file mode 100644 index 000000000..76b3960ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -0,0 +1,20 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveFlag : CharacterAbility + { + private readonly AbilityFlags abilityFlag; + + // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types + public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.AddAbilityFlag(abilityFlag); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs new file mode 100644 index 000000000..9c4dd581a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -0,0 +1,46 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveMoney : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly int amount; + private readonly string scalingStatIdentifier; + + public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty); + } + + private void ApplyEffectSpecific(Character targetCharacter) + { + float multiplier = 1f; + if (!string.IsNullOrEmpty(scalingStatIdentifier)) + { + multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); + } + + targetCharacter.GiveMoney((int)(multiplier * amount)); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffectSpecific(Character); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs new file mode 100644 index 000000000..6fb4887b3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -0,0 +1,64 @@ +using Barotrauma.Extensions; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGivePermanentStat : CharacterAbility + { + private readonly string statIdentifier; + private readonly StatTypes statType; + private readonly float value; + private readonly float maxValue; + private readonly bool targetAllies; + private readonly bool removeOnDeath; + private readonly bool giveOnAddingFirstTime; + private readonly bool setValue; + + //private readonly float maximumValue; + public override bool AllowClientSimulation => true; + public override bool AppliesEffectOnIntervalUpdate => true; + + public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); + statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue); + targetAllies = abilityElement.GetAttributeBool("targetallies", false); + removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); + giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); + setValue = abilityElement.GetAttributeBool("setvalue", false); + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (giveOnAddingFirstTime && addingFirstTime) + { + ApplyEffectSpecific(); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + if (targetAllies) + { + Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue)); + } + else + { + Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs new file mode 100644 index 000000000..253dd787b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveResistance : CharacterAbility + { + private readonly string resistanceId; + private readonly float multiplier; + + public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + resistanceId = abilityElement.GetAttributeString("resistanceid", abilityElement.GetAttributeString("resistance", string.Empty)); + multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency + + if (string.IsNullOrEmpty(resistanceId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); + } + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.ChangeAbilityResistance(resistanceId, multiplier); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs new file mode 100644 index 000000000..c999d3999 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -0,0 +1,21 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveStat : CharacterAbility + { + private readonly StatTypes statType; + private readonly float value; + + public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + public override void InitializeAbility(bool addingFirstTime) + { + Character.ChangeStat(statType, value); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs new file mode 100644 index 000000000..8ba1c9ef9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -0,0 +1,23 @@ +using Barotrauma.Extensions; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveTalentPoints : CharacterAbility + { + private readonly int amount; + + public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (addingFirstTime && Character.Info != null) + { + Character.Info.AdditionalTalentPoints += amount; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs new file mode 100644 index 000000000..d93519de0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -0,0 +1,60 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityIncreaseSkill : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly string skillIdentifier; + private readonly float skillIncrease; + + public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f); + + if (string.IsNullOrEmpty(skillIdentifier)) + { + DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill."); + } + if (MathUtils.NearlyEqual(skillIncrease, 0)) + { + DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0."); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character character) + { + ApplyEffectSpecific(character); + } + else + { + ApplyEffectSpecific(Character); + } + } + + private void ApplyEffectSpecific(Character character) + { + if (skillIdentifier.Equals("random")) + { + var skill = character.Info?.Job?.Skills?.GetRandom(); + if (skill == null) { return; } + character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease); + } + else + { + character.Info?.IncreaseSkillLevel(skillIdentifier, skillIncrease); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs new file mode 100644 index 000000000..5d073a068 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyAffliction : CharacterAbility + { + private readonly string[] afflictionIdentifiers; + + private readonly float addedMultiplier; + + public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); + addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + { + foreach (string afflictionIdentifier in afflictionIdentifiers) + { + if (affliction.Identifier == afflictionIdentifier) + { + affliction.Strength *= 1 + addedMultiplier; + } + } + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs new file mode 100644 index 000000000..2e742bb3f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyAttackData : CharacterAbility + { + private readonly List afflictions; + + private readonly float addedDamageMultiplier; + private readonly float addedPenetration; + private readonly bool implode; + + public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + if (abilityElement.GetChildElement("afflictions") is XElement afflictionElements) + { + afflictions = CharacterAbilityGroup.ParseAfflictions(CharacterTalent, afflictionElements); + } + addedDamageMultiplier = abilityElement.GetAttributeFloat("addeddamagemultiplier", 0f); + addedPenetration = abilityElement.GetAttributeFloat("addedpenetration", 0f); + implode = abilityElement.GetAttributeBool("implode", false); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is AbilityAttackData attackData) + { + if (attackData.Afflictions == null) + { + attackData.Afflictions = afflictions; + } + else + { + attackData.Afflictions.AddRange(afflictions); + } + attackData.DamageMultiplier += addedDamageMultiplier; + attackData.AddedPenetration += addedPenetration; + + if (implode) + { + // might have issues, as the method used to be private and only used for pressure death + attackData.Character?.Implode(); + } + + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs new file mode 100644 index 000000000..d9953bf23 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -0,0 +1,34 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyFlag : CharacterAbility + { + private readonly AbilityFlags abilityFlag; + + private bool lastState; + + public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + if (conditionsMatched) + { + Character.AddAbilityFlag(abilityFlag); + } + else + { + Character.RemoveAbilityFlag(abilityFlag); + } + + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs new file mode 100644 index 000000000..affb06085 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyReduceAffliction.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyReduceAffliction : CharacterAbility + { + float addedAmountMultiplier; + + public CharacterAbilityModifyReduceAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedAmountMultiplier = abilityElement.GetAttributeFloat("addedamountmultiplier", 0f); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is AbilityValueAffliction afflictionReduceAmount) + { + afflictionReduceAmount.Affliction.Strength -= addedAmountMultiplier * afflictionReduceAmount.Value; + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs new file mode 100644 index 000000000..4317f6745 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -0,0 +1,32 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyResistance : CharacterAbility + { + private readonly string resistanceId; + private readonly float resistance; + bool lastState; + + // should probably be split to different classes + public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistance = abilityElement.GetAttributeFloat("resistance", 1f); + + if (string.IsNullOrEmpty(resistanceId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); + } + } + + public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance); + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs new file mode 100644 index 000000000..c61a5a646 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStat : CharacterAbility + { + private readonly StatTypes statType; + private readonly float value; + bool lastState; + + public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched != lastState) + { + Character.ChangeStat(statType, conditionsMatched ? value : -value); + lastState = conditionsMatched; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs new file mode 100644 index 000000000..20dbf654d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStatToFlooding : CharacterAbility + { + private readonly StatTypes statType; + private readonly float maxValue; + private float lastValue = 0f; + + public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + Character.ChangeStat(statType, -lastValue); + + if (conditionsMatched && Character.IsInFriendlySub) + { + float currentFloodPercentage = Character.Submarine.GetHulls(false).Average(h => h.WaterPercentage); + lastValue = currentFloodPercentage / 100f * maxValue; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs new file mode 100644 index 000000000..a8630dc2c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStatToLevel : CharacterAbility + { + private readonly StatTypes statType; + private readonly float statPerLevel; + private readonly int maxLevel; + private float lastValue = 0f; + + public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + statPerLevel = abilityElement.GetAttributeFloat("statperlevel", 0f); + maxLevel = abilityElement.GetAttributeInt("maxlevel", int.MaxValue); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + Character.ChangeStat(statType, -lastValue); + if (conditionsMatched) + { + int level = MathHelper.Min(Character?.Info.GetCurrentLevel() ?? 0, maxLevel); + lastValue = statPerLevel * level; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs new file mode 100644 index 000000000..44435e95d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStatToSkill : CharacterAbility + { + private readonly StatTypes statType; + private readonly float maxValue; + private readonly string skillIdentifier; + private readonly bool useAll; + private float lastValue = 0f; + + public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); + skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty); + useAll = skillIdentifier == "all"; + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + Character.ChangeStat(statType, -lastValue); + + if (conditionsMatched) + { + float skillTotal = 0f; + + if (useAll && Character.Info?.Job != null) + { + foreach (Skill skill in Character.Info.Job.Skills) + { + skillTotal += Character.GetSkillLevel(skill.Identifier); + } + skillTotal /= Character.Info.Job.Skills.Count; + } + else + { + skillTotal = Character.GetSkillLevel(skillIdentifier); + } + + lastValue = skillTotal / 100f * maxValue; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs new file mode 100644 index 000000000..59a203a7f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -0,0 +1,25 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyValue : CharacterAbility + { + private readonly float addedValue; + private readonly float multiplyValue; + + public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); + multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is IAbilityValue abilityValue) + { + abilityValue.Value += addedValue; + abilityValue.Value *= multiplyValue; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs new file mode 100644 index 000000000..e80bf63db --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -0,0 +1,46 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityPutItem : CharacterAbility + { + private readonly string itemIdentifier; + private readonly int amount; + public override bool AppliesEffectOnIntervalUpdate => true; + public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + itemIdentifier = abilityElement.GetAttributeString("itemidentifier", ""); + amount = abilityElement.GetAttributeInt("amount", 1); + } + + protected override void ApplyEffect() + { + if (string.IsNullOrEmpty(itemIdentifier)) + { + DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); + return; + } + + ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier); + if (itemPrefab == null) + { + DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found."); + return; + } + for (int i = 0; i < amount; i++) + { + if (GameMain.GameSession?.RoundEnding ?? true) + { + Item item = new Item(itemPrefab, Character.WorldPosition, Character.Submarine); + Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }); + } + else + { + Entity.Spawner.AddToSpawnQueue(itemPrefab, Character.Inventory); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs new file mode 100644 index 000000000..3716a6fbc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -0,0 +1,30 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityResetPermanentStat : CharacterAbility + { + private readonly string statIdentifier; + public override bool AppliesEffectOnIntervalUpdate => true; + public override bool AllowClientSimulation => true; + + public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + } + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + private void ApplyEffectSpecific() + { + Character?.Info.ResetSavedStatValue(statIdentifier); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs new file mode 100644 index 000000000..7ed61e90f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityRevive : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + private void ApplyEffectSpecific() + { + Character.Revive(removeAllAfflictions: false); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffectSpecific(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs new file mode 100644 index 000000000..b13e97638 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilitySpawnItemsToContainer : CharacterAbility + { + // currently used only for spawning items to containers + + private readonly List statusEffects; + private readonly List openedContainers = new List(); + private readonly float randomChance; + private readonly bool oncePerContainer; + + public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); + oncePerContainer = abilityElement.GetAttributeBool("oncepercontainer", false); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + if (oncePerContainer) + { + if (openedContainers.Contains(item)) { return; } + openedContainers.Add(item); + } + if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + + foreach (var statusEffect in statusEffects) + { + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, item, item); + } + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs new file mode 100644 index 000000000..b9f26160c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityUnlockTree : CharacterAbility + { + public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + + var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + if (subTree != null) + { + foreach (var talentOption in subTree.TalentOptionStages) + { + foreach (var talent in talentOption.Talents) + { + if (talent == CharacterTalent.Prefab) { continue; } + Character.GiveTalent(talent); + } + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs new file mode 100644 index 000000000..7c7141a25 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityAlienHoarder : CharacterAbility + { + private readonly float addedDamageMultiplierPerItem; + private readonly float maxAddedDamageMultiplier; + private readonly string[] tags; + + public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f); + maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue); + tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is AbilityAttackData attackData) + { + float totalAddedDamageMultiplier = 0f; + foreach (Item item in Character.Inventory.AllItems) + { + if (tags.Any(t => item.Prefab.Tags.Any(p => t == p))) + { + totalAddedDamageMultiplier += addedDamageMultiplierPerItem; + } + } + attackData.DamageMultiplier += Math.Min(totalAddedDamageMultiplier, maxAddedDamageMultiplier); + } + else + { + LogabilityObjectMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs new file mode 100644 index 000000000..95466f08d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApprenticeship : CharacterAbility + { + public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character) + { + Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromApprenticeship: true); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs new file mode 100644 index 000000000..5ab9361b8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityAtmosMachine : CharacterAbility + { + private readonly float addedValue; + private readonly float multiplyValue; + private readonly string[] tags; + private readonly int maxMultiplyCount; + + public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); + multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); + tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is IAbilityValue abilityValue) + { + int multiplyCount = 0; + + foreach (Item item in Item.ItemList) + { + if (item.Prefab.Tags.Any(t => tags.Contains(t))) + { + multiplyCount++; + if (multiplyCount == maxMultiplyCount) + { + break; + } + } + } + abilityValue.Value += addedValue * multiplyCount; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs new file mode 100644 index 000000000..417b93972 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityBountyHunter : CharacterAbility + { + private float vitalityPercentage; + + public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character character) + { + Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs new file mode 100644 index 000000000..8bfe07b9d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityByTheBook : CharacterAbility + { + private readonly int moneyAmount; + private readonly int experienceAmount; + private readonly int max; + + public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + moneyAmount = abilityElement.GetAttributeInt("moneyamount", 0); + experienceAmount = abilityElement.GetAttributeInt("experienceamount", 0); + max = abilityElement.GetAttributeInt("max", 0); + } + + protected override void ApplyEffect() + { + IEnumerable enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None); + + int timesGiven = 0; + foreach (Character enemyCharacter in enemyCharacters) + { + if (!enemyCharacter.IsHuman) { continue; } + if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; } + if (enemyCharacter.IsDead) { continue; } + if (!enemyCharacter.LockHands) { continue; } + if (timesGiven > max) { continue; } + Character.GiveMoney(moneyAmount); + foreach (Character character in Character.GetFriendlyCrew(Character)) + { + character.Info?.GiveExperience(experienceAmount); + } + timesGiven++; + } + + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs new file mode 100644 index 000000000..1583bd53b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -0,0 +1,31 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityInsurancePolicy : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly int moneyPerMission; + + private static List clientsAlreadyUsed = new List(); + + public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); + } + + protected override void ApplyEffect() + { + if (Character?.Info is CharacterInfo info) + { + + Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs new file mode 100644 index 000000000..339b5c47f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityMultitasker : CharacterAbility + { + private string lastSkillIdentifier; + + public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + { + if (skillIdentifier != lastSkillIdentifier) + { + lastSkillIdentifier = skillIdentifier; + Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs new file mode 100644 index 000000000..61e9d9cf6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityPsychoClown : CharacterAbility + { + private StatTypes statType; + private float maxValue; + private string afflictionIdentifier; + private float lastValue = 0f; + + public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); + afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", ""); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + // managing state this way seems liable to cause bugs, maybe instead create abstraction to reset these values more safely + // talents cannot be removed while in active play because of the lack of this, for example + Character.ChangeStat(statType, -lastValue); + + if (conditionsMatched) + { + var affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier); + + float afflictionStrength = 0f; + if (affliction != null) + { + afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength; + } + + lastValue = afflictionStrength * maxValue; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs new file mode 100644 index 000000000..574d9d7b1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -0,0 +1,39 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityRegenerateLoot : CharacterAbility + { + // separate random chance used for the ability itself to prevent the player + // from opening/reopening a container until it spawns loot + private readonly float randomChance; + + // not maintained through death, so it's possible for players to respawn and re-loot chests + // seems like a minor issue for now + private readonly List openedContainers = new List(); + + public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + if (openedContainers.Contains(item)) { return; } + openedContainers.Add(item); + if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } + + if (item.GetComponent() is ItemContainer itemContainer) + { + AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs new file mode 100644 index 000000000..75668b0ca --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -0,0 +1,43 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly + { + // this should just be its own class, misleading to inherit here + private string tag; + public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + tag = abilityElement.GetAttributeString("tag", ""); + } + + protected override void ApplyEffect() + { + if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } + + Character closestCharacter = null; + float closestDistance = float.MaxValue; + + foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) + { + if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance) + { + closestCharacter = crewCharacter; + closestDistance = tempDistance; + } + } + + if (closestCharacter.SelectedConstruction == null || !closestCharacter.SelectedConstruction.HasTag(tag)) { return; } + + if (closestDistance < squaredMaxDistance) + { + ApplyEffectSpecific(Character); + ApplyEffectSpecific(closestCharacter); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs new file mode 100644 index 000000000..7c96b3d17 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + abstract class CharacterAbilityGroup + { + public CharacterTalent CharacterTalent { get; } + public Character Character { get; } + + // currently only used to turn off simulation if random conditions are in use + public bool IsActive { get; private set; } = true; + + public readonly AbilityEffectType AbilityEffectType; + + protected int maxTriggerCount { get; } + protected int timesTriggered = 0; + + + // add support for OR conditions? + protected readonly List abilityConditions = new List(); + + // separate dictionaries for each type of characterability? + protected readonly List characterAbilities = new List(); + + public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) + { + AbilityEffectType = abilityEffectType; + CharacterTalent = characterTalent; + Character = CharacterTalent.Character; + maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); + foreach (XElement subElement in abilityElementGroup.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "abilities": + LoadAbilities(subElement); + break; + case "conditions": + LoadConditions(subElement); + break; + } + } + } + + public void ActivateAbilityGroup(bool addingFirstTime) + { + foreach (var characterAbility in characterAbilities) + { + characterAbility.InitializeAbility(addingFirstTime); + } + } + + public void LoadConditions(XElement conditionElements) + { + foreach (XElement conditionElement in conditionElements.Elements()) + { + AbilityCondition newCondition = ConstructCondition(CharacterTalent, conditionElement); + + if (newCondition == null) + { + DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + if (!newCondition.AllowClientSimulation && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + IsActive = false; + } + + abilityConditions.Add(newCondition); + } + } + + public void AddAbility(CharacterAbility characterAbility) + { + if (characterAbility == null) + { + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + characterAbilities.Add(characterAbility); + } + + // XML + private AbilityCondition ConstructCondition(CharacterTalent characterTalent, XElement conditionElement, bool errorMessages = true) + { + AbilityCondition newCondition = null; + + Type conditionType; + string type = conditionElement.Name.ToString().ToLowerInvariant(); + try + { + conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true); + if (conditionType == null) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")"); + return null; + } + } + catch (Exception e) + { + if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e); + return null; + } + + object[] args = { characterTalent, conditionElement }; + + try + { + newCondition = (AbilityCondition)Activator.CreateInstance(conditionType, args); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException); + return null; + } + + if (newCondition == null) + { + DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null"); + return null; + } + + return newCondition; + } + + private void LoadAbilities(XElement abilityElements) + { + foreach (XElement abilityElementGroup in abilityElements.Elements()) + { + AddAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); + } + } + + private CharacterAbility ConstructAbility(XElement abilityElement, CharacterTalent characterTalent) + { + CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); + + if (newAbility == null) + { + DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!"); + return null; + } + + return newAbility; + } + + public static List ParseStatusEffects(CharacterTalent characterTalent, XElement statusEffectElements) + { + if (statusEffectElements == null) + { + DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier); + return null; + } + + List statusEffects = new List(); + + foreach (XElement statusEffectElement in statusEffectElements.Elements()) + { + var statusEffect = StatusEffect.Load(statusEffectElement, characterTalent.DebugIdentifier); + statusEffects.Add(statusEffect); + } + + return statusEffects; + } + + public static StatTypes ParseStatType(string statTypeString, string debugIdentifier) + { + if (!Enum.TryParse(statTypeString, true, out StatTypes statType)) + { + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); + } + return statType; + } + + public static List ParseAfflictions(CharacterTalent characterTalent, XElement afflictionElements) + { + if (afflictionElements == null) + { + DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier); + return null; + } + + List afflictions = new List(); + + // similar logic to affliction creation in statuseffects + // might be worth unifying + + foreach (XElement afflictionElement in afflictionElements.Elements()) + { + string afflictionIdentifier = afflictionElement.GetAttributeString("identifier", "").ToLowerInvariant(); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + if (afflictionPrefab == null) + { + DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); + continue; + } + + Affliction afflictionInstance = afflictionPrefab.Instantiate(afflictionElement.GetAttributeFloat(1.0f, "amount", "strength")); + afflictionInstance.Probability = afflictionElement.GetAttributeFloat(1.0f, "probability"); + afflictions.Add(afflictionInstance); + } + + return afflictions; + } + + public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier) + { + AbilityFlags flagType = AbilityFlags.None; + if (!Enum.TryParse(flagTypeString, true, out flagType)) + { + DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); + } + return flagType; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs new file mode 100644 index 000000000..e4d488103 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGroupEffect : CharacterAbilityGroup + { + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + base(abilityEffectType, characterTalent, abilityElementGroup) { } + + public void CheckAbilityGroup(AbilityObject abilityObject) + { + if (!IsActive) { return; } + if (IsApplicable(abilityObject)) + { + foreach (var characterAbility in characterAbilities) + { + if (characterAbility.IsViable()) + { + characterAbility.ApplyAbilityEffect(abilityObject); + } + } + timesTriggered++; + } + } + + private bool IsApplicable(AbilityObject abilityObject) + { + if (timesTriggered >= maxTriggerCount) { return false; } + return abilityConditions.All(c => c.MatchesCondition(abilityObject)); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs new file mode 100644 index 000000000..c7a302149 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGroupInterval : CharacterAbilityGroup + { + private float interval { get; set; } + public float TimeSinceLastUpdate { get; private set; } + + private float effectDelay; + private float effectDelayTimer; + + + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + base(abilityEffectType, characterTalent, abilityElementGroup) + { + // too many overlapping intervals could cause hitching? maybe randomize a little + interval = abilityElementGroup.GetAttributeFloat("interval", 0f); + effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f); + } + public void UpdateAbilityGroup(float deltaTime) + { + if (!IsActive) { return; } + TimeSinceLastUpdate += deltaTime; + if (TimeSinceLastUpdate >= interval) + { + bool conditionsMatched = IsApplicable(); + effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f; + conditionsMatched &= effectDelayTimer >= effectDelay; + + foreach (var characterAbility in characterAbilities) + { + if (characterAbility.IsViable()) + { + characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); + } + } + if (conditionsMatched) + { + timesTriggered++; + } + TimeSinceLastUpdate = 0; + } + } + private bool IsApplicable() + { + if (timesTriggered >= maxTriggerCount) { return false; } + return abilityConditions.All(c => c.MatchesCondition()); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs new file mode 100644 index 000000000..3a79e1b2a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Abilities; + +namespace Barotrauma +{ + class CharacterTalent + { + public Character Character { get; } + public string DebugIdentifier { get; } + + public readonly TalentPrefab Prefab; + + public bool AddedThisRound = true; + + private readonly Dictionary> characterAbilityGroupEffectDictionary = new Dictionary>(); + + private readonly List characterAbilityGroupIntervals = new List(); + + // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well + public List UnlockedRecipes { get; } = new List(); + + public CharacterTalent(TalentPrefab talentPrefab, Character character) + { + Character = character; + + Prefab = talentPrefab; + XElement element = talentPrefab.ConfigElement; + DebugIdentifier = talentPrefab.OriginalName; + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "abilitygroupeffect": + LoadAbilityGroupEffect(subElement); + break; + case "abilitygroupinterval": + LoadAbilityGroupInterval(subElement); + break; + case "addedrecipe": + if (subElement.GetAttributeString("itemidentifier", string.Empty) is string recipeIdentifier && recipeIdentifier != string.Empty) + { + UnlockedRecipes.Add(recipeIdentifier); + } + else + { + DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier); + } + break; + } + } + } + + public virtual void UpdateTalent(float deltaTime) + { + foreach (var characterAbilityGroupInterval in characterAbilityGroupIntervals) + { + characterAbilityGroupInterval.UpdateAbilityGroup(deltaTime); + } + } + + public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups)) + { + foreach (var characterAbilityGroup in characterAbilityGroups) + { + characterAbilityGroup.CheckAbilityGroup(abilityObject); + } + } + } + + public void ActivateTalent(bool addingFirstTime) + { + foreach (var characterAbilityGroups in characterAbilityGroupEffectDictionary.Values) + { + foreach (var characterAbilityGroup in characterAbilityGroups) + { + characterAbilityGroup.ActivateAbilityGroup(addingFirstTime); + } + } + } + + // XML logic + private void LoadAbilityGroupInterval(XElement abilityGroup) + { + characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(AbilityEffectType.Undefined, this, abilityGroup)); + } + + private void LoadAbilityGroupEffect(XElement abilityGroup) + { + AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none")); + AddAbilityGroupEffect(new CharacterAbilityGroupEffect(abilityEffectType, this, abilityGroup), abilityEffectType); + } + + public void AddAbilityGroupEffect(CharacterAbilityGroupEffect characterAbilityGroup, AbilityEffectType abilityEffectType = AbilityEffectType.None) + { + if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityList)) + { + characterAbilityList.Add(characterAbilityGroup); + } + else + { + List characterAbilityGroups = new List(); + characterAbilityGroups.Add(characterAbilityGroup); + characterAbilityGroupEffectDictionary.Add(abilityEffectType, characterAbilityGroups); + } + } + + public static AbilityEffectType ParseAbilityEffectType(CharacterTalent characterTalent, string abilityEffectTypeString) + { + if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType)) + { + DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + if (abilityEffectType == AbilityEffectType.Undefined) + { + DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); + } + + return abilityEffectType; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs new file mode 100644 index 000000000..277af65b0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TalentPrefab : IPrefab, IDisposable, IHasUintIdentifier + { + public string Identifier { get; private set; } + public string OriginalName => Identifier; + public ContentPackage ContentPackage { get; private set; } + public string FilePath { get; private set; } + + public string DisplayName { get; private set; } + + public string Description { get; private set; } + + public readonly Sprite Icon; + + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); + + public XElement ConfigElement + { + get; + private set; + } + + public TalentPrefab(XElement element, string filePath) + { + FilePath = filePath; + ConfigElement = element; + Identifier = element.GetAttributeString("identifier", "noidentifier"); + DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier; + this.CalculatePrefabUIntIdentifier(TalentPrefabs); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "icon": + Icon = new Sprite(subElement); + break; + case "description": + string tempDescription = Description; + TextManager.ConstructDescription(ref tempDescription, subElement); + Description = tempDescription; + break; + } + } + + if (string.IsNullOrEmpty(Description)) + { + if (element.Attribute("description") != null) + { + string description = element.GetAttributeString("description", string.Empty); + Description = TextManager.Get(description, returnNull: true) ?? description; + } + else + { + Description = TextManager.Get("talentdescription." + Identifier, returnNull: true) ?? string.Empty; + } + } + +#if DEBUG + if (!TextManager.ContainsTag("talentname." + Identifier)) + { + DebugConsole.AddWarning($"Name for the talent \"{Identifier}\" not found in the text files."); + } + if (string.IsNullOrEmpty(Description)) + { + DebugConsole.AddWarning($"Description for the talent \"{Identifier}\" not configured"); + } + if (Description.Contains('[')) + { + DebugConsole.ThrowError($"Description for the talent \"{Identifier}\" contains brackets - was some variable not replaced correctly? ({Description})"); + } +#endif + } + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + disposed = true; + TalentPrefabs.Remove(this); + } + + /// + /// Unique identifier that's generated by hashing the prefab's string identifier. + /// Used to reduce the amount of bytes needed to write talent data into network messages in multiplayer. + /// + public uint UIntIdentifier { get; set; } + + public static void RemoveByFile(string filePath) => TalentPrefabs.RemoveByFile(filePath); + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("Loading talent prefab: " + file.Path); + RemoveByFile(file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "talent": + TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), false); + break; + case "talents": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var itemElement = element.GetChildElement("talent"); + if (itemElement != null) + { + TalentPrefabs.Add(new TalentPrefab(rootElement, file.Path), true); + } + else + { + DebugConsole.ThrowError($"Cannot find a talent element from the children of the override element defined in {file.Path}"); + } + } + else + { + TalentPrefabs.Add(new TalentPrefab(element, file.Path), false); + } + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); + break; + } + } + + public static void LoadAll(IEnumerable files) + { + DebugConsole.Log("Loading talent prefabs: "); + + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs new file mode 100644 index 000000000..02377cba0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class TalentTree + { + public enum TalentTreeStageState + { + Invalid, + Locked, + Unlocked, + Available, + Highlighted + } + + public static readonly Dictionary JobTalentTrees = new Dictionary(); + + public readonly List TalentSubTrees = new List(); + + public XElement ConfigElement + { + get; + private set; + } + + public TalentTree(XElement element, string filePath) + { + ConfigElement = element; + + string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); + + if (string.IsNullOrEmpty(jobIdentifier)) + { + DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!"); + return; + } + + foreach (XElement subTreeElement in element.GetChildElements("subtree")) + { + TalentSubTrees.Add(new TalentSubTree(subTreeElement)); + } + + // talents found and unlocked using the identifier wihin the talent tree, so no duplicates may occur + HashSet duplicateSet = new HashSet(); + foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)))) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + DebugConsole.AddWarning($"Talent tree for job {jobIdentifier} contains non-existent talent {talent}! Talent tree not added."); + return; + } + if (!duplicateSet.Add(talent)) + { + DebugConsole.ThrowError($"Talent tree for job {jobIdentifier} contains duplicate talent {talent}! Talent tree not added."); + return; + } + } + + if (!JobTalentTrees.TryAdd(jobIdentifier, this)) + { + DebugConsole.ThrowError($"Could not add talent tree for job {jobIdentifier}! A talent tree for this job is already likely defined"); + } + } + + public bool TalentIsInTree(string talentIdentifier) + { + return TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))).Any(c => c == talentIdentifier); + } + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("Loading talent tree: " + file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "talenttree": + new TalentTree(rootElement, file.Path); + break; + case "talenttrees": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var treeElement = element.GetChildElement("talenttree"); + if (treeElement != null) + { + new TalentTree(rootElement, file.Path); + } + else + { + DebugConsole.ThrowError($"Cannot find a talent tree element from the children of the override element defined in {file.Path}"); + } + } + else + { + new TalentTree(element, file.Path); + } + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}"); + break; + } + } + + public static void LoadAll(IEnumerable files) + { + DebugConsole.Log("Loading talent tree: "); + + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + } + + public static bool IsViableTalentForCharacter(Character character, string talentIdentifier) + { + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? Enumerable.Empty()); + } + + // i hate this function - markus + public static TalentTreeStageState GetTalentOptionStageState(Character character, string subTreeIdentifier, int index, List selectedTalents) + { + if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } + + if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + + TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + + if (subTree == null) { return TalentTreeStageState.Invalid; } + + TalentOption targetTalentOption = subTree.TalentOptionStages[index]; + + if (targetTalentOption.Talents.Any(t => character.HasTalent(t.Identifier))) + { + return TalentTreeStageState.Unlocked; + } + + if (targetTalentOption.Talents.Any(t => selectedTalents.Contains(t.Identifier))) + { + return TalentTreeStageState.Highlighted; + } + + bool hasTalentInLastTier = true; + bool isLastTalentPurchased = true; + + int lastindex = index - 1; + if (lastindex >= 0) + { + TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex]; + hasTalentInLastTier = lastLatentOption.Talents.Any(HasTalent); + isLastTalentPurchased = lastLatentOption.Talents.Any(t => character.HasTalent(t.Identifier)); + } + + if (!hasTalentInLastTier) + { + return TalentTreeStageState.Locked; + } + + bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0; + + if (hasPointsForNewTalent) + { + return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available; + } + + return TalentTreeStageState.Locked; + + bool HasTalent(TalentPrefab t) + { + return selectedTalents.Contains(t.Identifier); + } + } + + + public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable selectedTalents) + { + if (character?.Info?.Job.Prefab == null) { return false; } + if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } + + if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + + foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var talentOptionStage in subTree.TalentOptionStages) + { + bool hasTalentInThisTier = talentOptionStage.Talents.Any(t => selectedTalents.Contains(t.Identifier)); + if (!hasTalentInThisTier) + { + if (talentOptionStage.Talents.Any(t => t.Identifier == talentIdentifier)) + { + return true; + } + else + { + break; + } + } + } + } + + return false; + } + + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) + { + List viableTalents = new List(); + bool canStillUnlock = true; + // keep trying to unlock talents until none of the talents are unlockable + while (canStillUnlock && selectedTalents.Any()) + { + canStillUnlock = false; + foreach (string talent in selectedTalents) + { + if (IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) + { + viableTalents.Add(talent); + canStillUnlock = true; + } + } + } + return viableTalents; + } + } + + class TalentSubTree + { + public string Identifier { get; } + + public string DisplayName { get; } + + public readonly List TalentOptionStages = new List(); + + public TalentSubTree(XElement subTreeElement) + { + Identifier = subTreeElement.GetAttributeString("identifier", ""); + + DisplayName = TextManager.Get("talenttree." + Identifier, returnNull: true) ?? Identifier; + + foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) + { + TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); + } + } + + } + + class TalentOption + { + public readonly List Talents = new List(); + + public TalentOption(XElement talentOptionsElement, string debugIdentifier) + { + foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + { + string identifier = talentOptionElement.GetAttributeString("identifier", string.Empty); + + if (!TalentPrefab.TalentPrefabs.ContainsKey(identifier)) + { + DebugConsole.ThrowError($"Error in talent tree \"{debugIdentifier}\" - could not find a talent with the identifier \"{identifier}\"."); + return; + } + Talents.Add(TalentPrefab.TalentPrefabs[identifier]); + } + } + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index ba609f244..dfcd2b32d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -51,7 +51,9 @@ namespace Barotrauma WreckAIConfig, UpgradeModules, MapCreature, - EnemySubmarine + EnemySubmarine, + Talents, + TalentTrees, } public class ContentPackage @@ -103,7 +105,8 @@ namespace Barotrauma ContentType.Corpses, ContentType.UpgradeModules, ContentType.MapCreature, - ContentType.EnemySubmarine + ContentType.EnemySubmarine, + ContentType.Talents, }; //at least one file of each these types is required in core content packages @@ -135,7 +138,8 @@ namespace Barotrauma ContentType.Orders, ContentType.Corpses, ContentType.UpgradeModules, - ContentType.EnemySubmarine + ContentType.EnemySubmarine, + ContentType.Talents, }; public static IEnumerable CorePackageRequiredFiles diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index f149339d6..2ee6801ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -58,12 +58,7 @@ namespace Barotrauma return handle; } - public static CoroutineHandle Invoke(Action action) - { - return StartCoroutine(DoInvokeAfter(action, 0.0f)); - } - - public static CoroutineHandle InvokeAfter(Action action, float delay) + public static CoroutineHandle Invoke(Action action, float delay = 0f) { return StartCoroutine(DoInvokeAfter(action, delay)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 14e9e24e7..12be8b7e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -192,11 +192,6 @@ namespace Barotrauma GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats; })); - commands.Add(new Command("createfilelist", "", (string[] args) => - { - UpdaterUtil.SaveFileList("filelist.xml"); - })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { @@ -218,7 +213,7 @@ namespace Barotrauma }; }, isCheat: true)); - commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", + commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", (string[] args) => { try @@ -581,7 +576,7 @@ namespace Barotrauma } })); - commands.Add(new Command("dumptofile", "", (string[] args) => + commands.Add(new Command("dumptofile", "findentityids [filename]: Outputs the contents of the debug console into a text file in the game folder. If the filename argument is omitted, \"consoleOutput.txt\" is used as the filename.", (string[] args) => { string filename = "consoleOutput.txt"; if (args.Length > 0) { filename = string.Join(" ", args); } @@ -608,7 +603,7 @@ namespace Barotrauma } })); - commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => + commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { if (args.Length < 2) { return; } @@ -628,19 +623,24 @@ namespace Barotrauma } bool relativeStrength = false; - if (args.Length > 2) + if (args.Length > 4) { - bool.TryParse(args[2], out relativeStrength); + bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = (relativeStrength || args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray()); + Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] }); if (targetCharacter != null) { + Limb targetLimb = targetCharacter.AnimController.MainLimb; + if (args.Length > 3) + { + targetLimb = targetCharacter.AnimController.Limbs.FirstOrDefault(l => l.type.ToString().Equals(args[3], StringComparison.OrdinalIgnoreCase)); + } if (relativeStrength) { afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength; } - targetCharacter.CharacterHealth.ApplyAffliction(targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); + targetCharacter.CharacterHealth.ApplyAffliction(targetLimb ?? targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength)); } }, () => @@ -649,7 +649,8 @@ namespace Barotrauma { AfflictionPrefab.List.Select(a => a.Name).ToArray(), new string[] { "1" }, - Character.CharacterList.Select(c => c.Name).ToArray() + Character.CharacterList.Select(c => c.Name).ToArray(), + Enum.GetNames(typeof(LimbType)).ToArray() }; }, isCheat: true)); @@ -807,13 +808,13 @@ namespace Barotrauma { foreach (Skill skill in character.Info.Job.Skills) { - character.Info.SetSkillLevel(skill.Identifier, level, character.WorldPosition); + character.Info.SetSkillLevel(skill.Identifier, level); } NewMessage($"Set all {character.Name}'s skills to {level}", Color.Green); } else { - character.Info.SetSkillLevel(skillIdentifier, level, character.WorldPosition); + character.Info.SetSkillLevel(skillIdentifier, level); NewMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", Color.Green); } } @@ -834,9 +835,129 @@ namespace Barotrauma commands.Add(new Command("water|editwater", "water/editwater: Toggle water editing. Allows adding water into rooms by holding the left mouse button and removing it by holding the right mouse button.", (string[] args) => { Hull.EditWater = !Hull.EditWater; - NewMessage(Hull.EditWater ? "Water editing on" : "Water editing off", Color.White); + NewMessage(Hull.EditWater ? "Water editing on" : "Water editing off", Color.White); }, isCheat: true)); + commands.Add(new Command("givetalent", "givetalent [talent] [player]: give the talent to the specified character. If the character argument is omitted, the talent is given to the controlled character.", (string[] args) => + { + if (args.Length == 0) { return; } + var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled; + if (character != null) + { + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => + c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (talentPrefab == null) + { + ThrowError($"Couldn't find the talent \"{args[0]}\"."); + return; + } + character.GiveTalent(talentPrefab); + NewMessage($"Gave talent \"{talentPrefab.DisplayName}\" to \"{character.Name}\"."); + } + }, + () => + { + List talentNames = new List(); + foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs) + { + talentNames.Add(talent.DisplayName); + } + + return new string[][] + { + talentNames.ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + }; + }, isCheat: true)); + + commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]] [character]: give the specified character all the talents of the specified class", (string[] args) => + { + var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled; + if (character == null) { return; } + + List talentTrees = new List(); + if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) + { + talentTrees.AddRange(TalentTree.JobTalentTrees.Values); + } + else + { + var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (job == null) + { + ThrowError($"Failed to find the job \"{args[0]}\"."); + return; + } + if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + { + ThrowError($"No talents configured for the job \"{args[0]}\"."); + return; + } + talentTrees.Add(talentTree); + } + + foreach (var talentTree in talentTrees) + { + foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var option in subTree.TalentOptionStages) + { + foreach (var talent in option.Talents) + { + character.GiveTalent(talent); + NewMessage($"Unlocked talent \"{talent.DisplayName}\"."); + } + } + } + } + }, + () => + { + List availableArgs = new List() { "All" }; + availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name)); + return new string[][] + { + availableArgs.ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + }; + }, isCheat: true)); + + commands.Add(new Command("giveexperience", "giveexperience [amount] [character]: Give experience to character.", (string[] args) => + { + if (args.Length < 1) + { + NewMessage($"Missing arguments. Expected at least 1 but got {args.Length} (experience, name)"); + return; + } + + string experienceString = args[0]; + var character = FindMatchingCharacter(args.Skip(1).ToArray()) ?? Character.Controlled; + + if (character?.Info == null) + { + NewMessage("Character is not valid."); + return; + } + + if (int.TryParse(experienceString, NumberStyles.Number, CultureInfo.InvariantCulture, out int experience)) + { + character.Info.GiveExperience(experience); + NewMessage($"Gave {character.Name} {experience} experience"); + } + else + { + NewMessage($"{experienceString} is not a valid value. Expected number."); + } + }, isCheat: true, getValidArgs: () => + { + return new[] + { + new string[] { "100" }, + Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + }; + })); + commands.Add(new Command("fire|editfire", "fire/editfire: Allows putting up fires by left clicking.", (string[] args) => { Hull.EditFire = !Hull.EditFire; @@ -948,7 +1069,7 @@ namespace Barotrauma string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(subName, StringComparison.OrdinalIgnoreCase)); } int count = 0; @@ -1014,7 +1135,7 @@ namespace Barotrauma if (args.Length == 0) { return; } if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - campaign.Map.CurrentLocation.Reputation.Value = reputation; + campaign.Map.CurrentLocation.Reputation.SetReputation(reputation); } else { @@ -1041,7 +1162,7 @@ namespace Barotrauma { if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - faction.Reputation.Value = reputation; + faction.Reputation.SetReputation(reputation); } else { @@ -1369,7 +1490,7 @@ namespace Barotrauma NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); }, isCheat: true)); - commands.Add(new Command("money", "", args => + commands.Add(new Command("money", "money [amount]: Gives the specified amount of money to the crew when a campaign is active.", args => { if (args.Length == 0) { return; } if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1489,8 +1610,8 @@ namespace Barotrauma { if (args.Length > 0) { - string packageName = string.Join(" ", args).ToLower(); - var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.ToLower() == packageName); + string packageName = string.Join(" ", args); + var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); if (package == null) { ThrowError("Content package \"" + packageName + "\" not found."); @@ -1764,13 +1885,15 @@ namespace Barotrauma } return; } -#if !DEBUG if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) { - ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!"); +#if DEBUG + AddWarning($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\". Executing the command anyway because this is a debug build."); +#else + ThrowError($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\"!"); return; - } #endif + } } #endif @@ -1972,9 +2095,18 @@ namespace Barotrauma return; } + int amount = 1; if (args.Length > 1) { - switch (args.Last()) + string spawnLocation = args.Last(); + if (args.Length > 2) + { + spawnLocation = args[^2]; + if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } + amount = Math.Min(amount, 100); + } + + switch (spawnLocation) { case "cursor": spawnPos = cursorPos; @@ -2002,37 +2134,40 @@ namespace Barotrauma spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition; } - if (spawnPos != null) + for (int i = 0; i < amount; i++) { - if (Entity.Spawner == null) + if (spawnPos != null) { - new Item(itemPrefab, spawnPos.Value, null); - } - else - { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); - } - } - else if (spawnInventory != null) - { - if (Entity.Spawner == null) - { - var spawnedItem = new Item(itemPrefab, Vector2.Zero, null); - spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots); - onItemSpawned(spawnedItem); - } - else - { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); - } - - static void onItemSpawned(Item item) - { - if (item.ParentInventory?.Owner is Character character) + if (Entity.Spawner == null) { - foreach (WifiComponent wifiComponent in item.GetComponents()) + new Item(itemPrefab, spawnPos.Value, null); + } + else + { + Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); + } + } + else if (spawnInventory != null) + { + if (Entity.Spawner == null) + { + var spawnedItem = new Item(itemPrefab, Vector2.Zero, null); + spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots); + onItemSpawned(spawnedItem); + } + else + { + Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); + } + + static void onItemSpawned(Item item) + { + if (item.ParentInventory?.Owner is Character character) { - wifiComponent.TeamID = character.TeamID; + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = character.TeamID; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index d0a91b108..3ac2e2dbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -12,16 +12,135 @@ public enum ActionType { - Always, OnPicked, OnUse, OnSecondaryUse, - OnWearing, OnContaining, OnContained, OnNotContained, - OnActive, OnFailure, OnBroken, - OnFire, InWater, NotInWater, - OnImpact, - OnEating, - OnDamaged, - OnSevered, - OnProduceSpawned, - OnOpen, OnClose, - OnDeath = OnBroken, + Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3, + OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7, + OnActive = 8, OnFailure = 9, OnBroken = 10, + OnFire = 11, InWater = 12, NotInWater = 13, + OnImpact = 14, + OnEating = 15, + OnDamaged = 16, + OnSevered = 17, + OnProduceSpawned = 18, + OnOpen = 19, OnClose = 20, + OnSpawn = 21, + OnSuccess = 22, + OnAbility = 23, + OnDeath = OnBroken } + + public enum AbilityEffectType + { + Undefined, + None, + OnAttack, + OnAttackResult, + OnAttacked, + OnAttackedResult, + OnGainSkillPoint, + OnAllyGainSkillPoint, + OnRepairComplete, + OnItemFabricationSkillGain, + OnItemFabricatedAmount, + OnAllyItemFabricatedAmount, + OnOpenItemContainer, + OnUseRangedWeapon, + OnReduceAffliction, + OnAddDamageAffliction, + OnSelfRagdoll, + OnRagdoll, + OnRoundEnd, + OnAnyMissionCompleted, + OnAllMissionsCompleted, + OnGiveOrder, + OnCrewKillCharacter, + OnKillCharacter, + OnDieToCharacter, + OnAllyGainMissionExperience, + OnGainMissionExperience, + OnGainMissionMoney, + OnLocationDiscovered, + OnItemDeconstructed, + OnItemDeconstructedMaterial, + OnItemDeconstructedInventory, + OnStopTinkering, + OnItemPicked, + OnGeneticMaterialCombinedOrRefined, + OnCrewGeneticMaterialCombinedOrRefined, + AfterSubmarineAttacked, + OnApplyTreatment, + } + + public enum StatTypes + { + None, + // Skills + ElectricalSkillBonus, + HelmSkillBonus, + MechanicalSkillBonus, + MedicalSkillBonus, + WeaponsSkillBonus, + // Character attributes + MaximumHealthMultiplier, + MovementSpeed, + WalkingSpeed, + SwimmingSpeed, + BuffDurationMultiplier, + DebuffDurationMultiplier, + MedicalItemEffectivenessMultiplier, + FlowResistance, + // Combat + AttackMultiplier, + TeamAttackMultiplier, + RangedAttackSpeed, + TurretAttackSpeed, + TurretPowerCostReduction, + TurretChargeSpeed, + MeleeAttackSpeed, + MeleeAttackMultiplier, + RangedAttackMultiplier, + RangedSpreadReduction, + // Utility + RepairSpeed, + DeconstructorSpeedMultiplier, + RepairToolStructureRepairMultiplier, + RepairToolStructureDamageMultiplier, + RepairToolDeattachTimeMultiplier, + MaxRepairConditionMultiplierMechanical, + MaxRepairConditionMultiplierElectrical, + IncreaseFabricationQuality, + GeneticMaterialRefineBonus, + GeneticMaterialTaintedProbabilityReductionOnCombine, + SkillGainSpeed, + // Tinker + TinkeringDuration, + TinkeringStrength, + TinkeringDamage, + // Misc + ReputationGainMultiplier, + MissionMoneyGainMultiplier, + ExperienceGainMultiplier, + MissionExperienceGainMultiplier, + ExtraMissionCount, + ExtraSpecialSalesCount, + ApplyTreatmentsOnSelfFraction, + MaxAttachableCount, + } + + public enum AbilityFlags + { + None, + MustWalk, + ImmuneToPressure, + IgnoredByEnemyAI, + MoveNormallyWhileDragging, + CanTinker, + CanTinkerFabricatorsAndDeconstructors, + TinkeringPowersDevices, + GainSkillPastMaximum, + RetainExperienceForNewCharacter, + AllowSecondOrderedTarget, + PowerfulCPR, + AlwaysStayConscious, + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 17ab504c6..2da290284 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -42,7 +40,7 @@ namespace Barotrauma var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); foreach (var target in targets) { - target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.Position + Vector2.UnitY * 150.0f); + target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs new file mode 100644 index 000000000..aecef1c64 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class GodModeAction : EventAction + { + [Serialize(true, true)] + public bool Enabled { get; set; } + + [Serialize("", true)] + public string TargetTag { get; set; } + + public GodModeAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + private bool isFinished = false; + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + var targets = ParentEvent.GetTargets(TargetTag); + foreach (var target in targets) + { + if (target != null && target is Character character) + { + character.GodMode = Enabled; + } + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GodModeAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + (Enabled ? "Enable godmode" : "Disable godmode"); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 2ed053c81..d4f86a1f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -50,7 +50,7 @@ namespace Barotrauma Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); if (faction != null) { - faction.Reputation.Value += Increase; + faction.Reputation.AddReputation(Increase); } else { @@ -64,14 +64,14 @@ namespace Barotrauma Location location = campaign.Map.CurrentLocation; if (location != null) { - location.Reputation.Value += Increase; + location.Reputation.AddReputation(Increase); IEnumerable locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location); foreach (Location connectedLocation in locations) { Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null"); if (connectedLocation.Reputation != null) { - connectedLocation.Reputation.Value += (Increase / 4); + connectedLocation.Reputation.AddReputation(Increase / 4); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 4737eba63..d42fb1cf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -226,11 +226,11 @@ namespace Barotrauma List potentialItems = SpawnLocation switch { SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub), - SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null && it.ParentRuin == null), - SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null), - SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon), + SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null), + SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost), + SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck), + SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin), + SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; @@ -252,11 +252,11 @@ namespace Barotrauma List potentialSpawnPoints = spawnLocation switch { SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null), - SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.ParentRuin == null), - SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null), - SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon), + SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null), + SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), + SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck), + SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin), + SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 97bd587e8..fea12aa2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -122,7 +122,7 @@ namespace Barotrauma { npcOrItem = npc; npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; - npc.RequireConsciousnessForCustomInteract = false; + npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated; #if CLIENT npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 3f8a0a93a..0e0e9a77c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -103,7 +103,7 @@ namespace Barotrauma selectedEvents.Clear(); activeEvents.Clear(); - pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) { @@ -179,7 +179,7 @@ namespace Barotrauma if (eventSet == null) { return; } if (eventSet.OncePerOutpost) { - foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.prefab)) + foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.Prefabs)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) { @@ -380,9 +380,13 @@ namespace Barotrauma { pendingEventSets.Clear(); selectedEvents.Clear(); + activeEvents.Clear(); + QueuedEvents.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); + + pathFinder = null; } private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) @@ -430,23 +434,31 @@ namespace Barotrauma } } - var suitablePrefabs = eventSet.EventPrefabs.FindAll(e => - string.IsNullOrEmpty(e.prefab.BiomeIdentifier) || - e.prefab.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); + bool isPrefabSuitable(EventPrefab p) + => string.IsNullOrEmpty(p.BiomeIdentifier) || + p.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase); + + var suitablePrefabSubsets = eventSet.EventPrefabs + .FindAll(p => p.Prefabs.Any(isPrefabSuitable)); for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { - if (suitablePrefabs.Count > 0) + if (suitablePrefabSubsets.Count > 0) { - var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(suitablePrefabs); + var unusedEvents = suitablePrefabSubsets.ToList(); for (int j = 0; j < eventSet.EventCount; j++) { - if (unusedEvents.All(e => CalculateCommonness(e.prefab, e.commonness) <= 0.0f)) { break; } - (EventPrefab eventPrefab, float commonness, float probability) = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e.prefab, e.commonness)).ToList(), rand); - if (eventPrefab != null && rand.NextDouble() <= probability) + if (unusedEvents.All(e => e.Prefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Prefabs.Max(p => CalculateCommonness(p, e.Commonness))).ToList(), rand); + (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; + if (eventPrefabs != null && rand.NextDouble() <= probability) { + var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); + var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); + var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -461,7 +473,7 @@ namespace Barotrauma selectedEvents.Add(eventSet, new List()); } selectedEvents[eventSet].Add(newEvent); - unusedEvents.Remove((eventPrefab, commonness, probability)); + unusedEvents.Remove(subEventPrefab); } } } @@ -476,9 +488,13 @@ namespace Barotrauma } else { - foreach ((EventPrefab eventPrefab, float commonness, float probability) in suitablePrefabs) + foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { if (rand.NextDouble() > probability) { continue; } + + var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); + var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); + var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -862,8 +878,9 @@ namespace Barotrauma private float CalculateDistanceTraveled() { - if (level == null) { return 0.0f; } + if (level == null || pathFinder == null) { return 0.0f; } var refEntity = GetRefEntity(); + if (refEntity == null) { return 0.0f; } Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition); var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) @@ -976,6 +993,7 @@ namespace Barotrauma return false; case SubmarineType.Wreck: case SubmarineType.BeaconStation: + case SubmarineType.Ruin: return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index b7632a54e..3938f6db0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -40,7 +40,7 @@ namespace Barotrauma BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); - TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); + TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 54d748b8e..7431b31b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; @@ -48,10 +49,10 @@ namespace Barotrauma List eventPrefabs = new List(PrefabList); foreach (var eventSet in List) { - eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.prefab)); + eventPrefabs.AddRange(eventSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); foreach (var childSet in eventSet.ChildSets) { - eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.prefab)); + eventPrefabs.AddRange(childSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); } } return eventPrefabs; @@ -98,7 +99,48 @@ namespace Barotrauma public readonly Dictionary Commonness; - public readonly List<(EventPrefab prefab, float commonness, float probability)> EventPrefabs; + public struct SubEventPrefab + { + public SubEventPrefab(string debugIdentifier, string[] prefabIdentifiers, float? commonness, float? probability) + { + EventPrefab tryFindPrefab(string id) + { + var prefab = PrefabList.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase)); + if (prefab is null) + { + DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\" - could not find the event prefab \"{id}\"."); + } + return prefab; + } + + this.Prefabs = prefabIdentifiers + .Select(tryFindPrefab) + .Where(p => p != null) + .ToImmutableArray(); + this.Commonness = commonness ?? this.Prefabs.Select(p => p.Commonness).Max(); + this.Probability = probability ?? this.Prefabs.Select(p => p.Probability).Max(); + } + + public SubEventPrefab(EventPrefab prefab, float commonness, float probability) + { + Prefabs = prefab.ToEnumerable().ToImmutableArray(); + Commonness = commonness; + Probability = probability; + } + + public readonly ImmutableArray Prefabs; + public readonly float Commonness; + public readonly float Probability; + + public void Deconstruct(out IEnumerable prefabs, out float commonness, out float probability) + { + prefabs = Prefabs; + commonness = Commonness; + probability = Probability; + } + } + + public readonly List EventPrefabs; public readonly List ChildSets; @@ -112,7 +154,7 @@ namespace Barotrauma { DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier; Commonness = new Dictionary(); - EventPrefabs = new List<(EventPrefab prefab, float commonness, float probability)>(); + EventPrefabs = new List(); ChildSets = new List(); BiomeIdentifier = element.GetAttributeString("biome", string.Empty); @@ -178,23 +220,20 @@ namespace Barotrauma //an element with just an identifier = reference to an event prefab if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase)) { - string identifier = subElement.GetAttributeString("identifier", ""); - var prefab = PrefabList.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (prefab == null) - { - DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\" - could not find the event prefab \"{identifier}\"."); - } - else - { - float commonness = subElement.GetAttributeFloat("commonness", prefab.Commonness); - float probability = subElement.GetAttributeFloat("probability", prefab.Probability); - EventPrefabs.Add((prefab, commonness, probability)); - } + string[] identifiers = subElement.GetAttributeStringArray("identifier", Array.Empty()); + + float commonness = subElement.GetAttributeFloat("commonness", -1f); + float probability = subElement.GetAttributeFloat("probability", -1f); + EventPrefabs.Add(new SubEventPrefab( + debugIdentifier, + identifiers, + commonness>=0f ? commonness : (float?)null, + probability>=0f ? probability : (float?)null)); } else { var prefab = new EventPrefab(subElement); - EventPrefabs.Add((prefab, prefab.Commonness, prefab.Probability)); + EventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); } break; } @@ -346,13 +385,13 @@ namespace Barotrauma { if (thisSet.ChooseRandom) { - var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(thisSet.EventPrefabs); + var unusedEvents = thisSet.EventPrefabs.ToList(); for (int i = 0; i < thisSet.EventCount; i++) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.commonness).ToList(), Rand.RandSync.Unsynced); - if (eventPrefab.prefab != null) + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab.Prefabs.Any(p => p != null)) { - AddEvent(stats, eventPrefab.prefab); + AddEvents(stats, eventPrefab.Prefabs); unusedEvents.Remove(eventPrefab); } } @@ -361,7 +400,7 @@ namespace Barotrauma { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvent(stats, eventPrefab.prefab); + AddEvents(stats, eventPrefab.Prefabs); } } foreach (var childSet in thisSet.ChildSets) @@ -370,6 +409,9 @@ namespace Barotrauma } } + static void AddEvents(EventDebugStats stats, IEnumerable eventPrefabs) + => eventPrefabs.ForEach(p => AddEvent(stats, p)); + static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab) { if (eventPrefab.EventType == typeof(MonsterEvent)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 55e7366cf..7869cd1b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -306,7 +306,7 @@ namespace Barotrauma case 0: if (items.All(it => it.Removed || it.Condition <= 0.0f) && - requireKill.All(c => c.Removed || c.IsDead) && + requireKill.All(c => c.Removed || c.IsDead || (c.LockHands && c.Submarine == Submarine.MainSub)) && requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) { State = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..993b0cd02 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,176 @@ +using Barotrauma.Extensions; +using Barotrauma.RuinGeneration; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + private readonly string[] targetItemIdentifiers; + private readonly string[] targetEnemyIdentifiers; + private readonly int minEnemyCount; + private readonly HashSet existingTargets = new HashSet(); + private readonly HashSet spawnedTargets = new HashSet(); + private readonly HashSet allTargets = new HashSet(); + + private Ruin TargetRuin { get; set; } + + public override IEnumerable SonarPositions + { + get + { + if (State == 0) + { + return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition); + } + else + { + return Enumerable.Empty(); + } + } + } + + public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetitems", new string[0], convertToLowerInvariant: true); + targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetenemies", new string[0], convertToLowerInvariant: true); + minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0); + } + + protected override void StartMissionSpecific(Level level) + { + existingTargets.Clear(); + spawnedTargets.Clear(); + allTargets.Clear(); + if (IsClient) { return; } + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + if (TargetRuin == null) + { + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); + return; + } + if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1) + { + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition"); + return; + } + foreach (var item in Item.ItemList) + { + if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; } + if (item.Submarine != TargetRuin.Submarine) { continue; } + existingTargets.Add(item); + allTargets.Add(item); + } + int existingEnemyCount = 0; + foreach (var character in Character.CharacterList) + { + if (string.IsNullOrEmpty(character.SpeciesName)) { continue; } + if (!targetEnemyIdentifiers.Contains(character.SpeciesName.ToLowerInvariant())) { continue; } + if (character.Submarine != TargetRuin.Submarine) { continue; } + existingTargets.Add(character); + allTargets.Add(character); + existingEnemyCount++; + } + if (existingEnemyCount < minEnemyCount) + { + var enemyPrefabs = new HashSet(); + foreach (string identifier in targetEnemyIdentifiers) + { + var prefab = CharacterPrefab.FindBySpeciesName(identifier); + if (prefab != null) + { + enemyPrefabs.Add(prefab); + } + else + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\""); + } + } + if (enemyPrefabs.None()) + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies"); + return; + } + for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) + { + var prefab = enemyPrefabs.GetRandom(); + var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandom(w => w.CurrentHull != null)?.WorldPosition; + if (!spawnPos.HasValue) + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned"); + return; + } + var newEnemy = Character.Create(prefab.Identifier, spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false); + spawnedTargets.Add(newEnemy); + allTargets.Add(newEnemy); + } + } +#if DEBUG + DebugConsole.NewMessage("********** CLEAR RUIN MISSION INFO **********"); + DebugConsole.NewMessage($"Existing item targets: {existingTargets.Count - existingEnemyCount}"); + DebugConsole.NewMessage($"Existing enemy targets: {existingEnemyCount}"); + DebugConsole.NewMessage($"Spawned enemy targets: {spawnedTargets.Count}"); +#endif + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + if (IsClient) { return; } + switch (State) + { + case 0: + if (!AllTargetsEliminated()) { return; } + State = 1; + break; + case 1: + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + State = 2; + break; + } + } + + private bool AllTargetsEliminated() + { + foreach (var target in allTargets) + { + if (target is Item targetItem) + { + if (!IsItemDestroyed(targetItem)) + { + return false; + } + } + else if (target is Character targetEnemy) + { + if (!IsEnemyDefeated(targetEnemy)) + { + return false; + } + } +#if DEBUG + else + { + DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}"); + } +#endif + } + return true; + } + + private bool IsItemDestroyed(Item item) => item == null || item.Removed || item.Condition <= 0.0f; + + private bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; + + public override void End() + { + if (State == 2) + { + GiveReward(); + completed = true; + } + failed = !completed && State > 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 463da3632..3fc58b81a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -91,7 +91,7 @@ namespace Barotrauma int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); for (int i = 0; i < amount; i++) { - CoroutineManager.InvokeAfter(() => + CoroutineManager.Invoke(() => { //round ended before the coroutine finished if (GameMain.GameSession == null || Level.Loaded == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index dcc9d3279..d51ccd3fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -140,6 +140,12 @@ namespace Barotrauma public override int GetReward(Submarine sub) { + // If we are not at the location of the mission, skip the calculation of the reward + if (GameMain.GameSession?.StartLocation != Locations[0]) + { + return calculatedReward; + } + bool missionsChanged = false; if (GameMain.GameSession?.StartLocation?.SelectedMissions != null) { @@ -192,55 +198,14 @@ namespace Barotrauma if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } - private ItemPrefab FindItemPrefab(XElement element) - { - ItemPrefab itemPrefab; - if (element.Attribute("name") != null) - { - DebugConsole.ThrowError("Error in cargo mission \"" + Name + "\" - use item identifiers instead of names to configure the items."); - string itemName = element.GetAttributeString("name", ""); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found"); - } - } - else - { - string itemIdentifier = element.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found"); - } - } - return itemPrefab; - } - - private void LoadItemAsChild(XElement element, Item parent) { ItemPrefab itemPrefab = FindItemPrefab(element); - WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); - if (cargoSpawnPos == null) - { - DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found"); - return; - } + Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); + if (!position.HasValue) { return; } - var cargoRoom = cargoSpawnPos.CurrentHull; - if (cargoRoom == null) - { - DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); - return; - } - - Vector2 position = new Vector2( - cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), - cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); - - var item = new Item(itemPrefab, position, cargoRoom.Submarine) + var item = new Item(itemPrefab, position.Value, cargoRoomSub) { SpawnedInOutpost = true, AllowStealing = false diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index 3b077be2c..cb8508f9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -13,16 +13,5 @@ namespace Barotrauma { State = 1; } - -#if CLIENT - public override void ClientReadInitial(IReadMessage msg) - { - } -#elif SERVER - - public override void ServerWriteInitial(IWriteMessage msg, Client c) - { - } -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 04278f4d6..e407ba68d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -9,10 +9,10 @@ namespace Barotrauma { partial class MineralMission : Mission { - private Dictionary> ResourceClusters { get; } = new Dictionary>(); - private Dictionary> SpawnedResources { get; } = new Dictionary>(); - private Dictionary RelevantLevelResources { get; } = new Dictionary(); - private List> MissionClusterPositions { get; } = new List>(); + private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary> spawnedResources = new Dictionary>(); + private readonly Dictionary relevantLevelResources = new Dictionary(); + private readonly List> missionClusterPositions = new List>(); private readonly HashSet caves = new HashSet(); @@ -20,8 +20,8 @@ namespace Barotrauma { get { - return MissionClusterPositions - .Where(p => SpawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(SpawnedResources[p.Item1])) + return missionClusterPositions + .Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1])) .Select(p => p.Item2); } } @@ -33,53 +33,53 @@ namespace Barotrauma { var identifier = c.GetAttributeString("identifier", null); if (string.IsNullOrWhiteSpace(identifier)) { continue; } - if (ResourceClusters.ContainsKey(identifier)) + if (resourceClusters.ContainsKey(identifier)) { - ResourceClusters[identifier].First++; + resourceClusters[identifier] = (resourceClusters[identifier].amount + 1, resourceClusters[identifier].rotation); } else { - ResourceClusters.Add(identifier, new Pair(1, 0.0f)); + resourceClusters.Add(identifier, (1, 0.0f)); } } } protected override void StartMissionSpecific(Level level) { - if (SpawnedResources.Any()) + if (spawnedResources.Any()) { #if DEBUG - throw new Exception($"SpawnedResources.Count > 0 ({SpawnedResources.Count})"); + throw new Exception($"SpawnedResources.Count > 0 ({spawnedResources.Count})"); #else DebugConsole.AddWarning("Spawned resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - SpawnedResources.Clear(); + spawnedResources.Clear(); #endif } - if (RelevantLevelResources.Any()) + if (relevantLevelResources.Any()) { #if DEBUG - throw new Exception($"RelevantLevelResources.Count > 0 ({RelevantLevelResources.Count})"); + throw new Exception($"RelevantLevelResources.Count > 0 ({relevantLevelResources.Count})"); #else DebugConsole.AddWarning("Relevant level resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - RelevantLevelResources.Clear(); + relevantLevelResources.Clear(); #endif } - if (MissionClusterPositions.Any()) + if (missionClusterPositions.Any()) { #if DEBUG - throw new Exception($"MissionClusterPositions.Count > 0 ({MissionClusterPositions.Count})"); + throw new Exception($"MissionClusterPositions.Count > 0 ({missionClusterPositions.Count})"); #else DebugConsole.AddWarning("Mission cluster positions list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - MissionClusterPositions.Clear(); + missionClusterPositions.Clear(); #endif } caves.Clear(); if (IsClient) { return; } - foreach (var kvp in ResourceClusters) + foreach (var kvp in resourceClusters) { var prefab = ItemPrefab.Find(null, kvp.Key); if (prefab == null) @@ -88,15 +88,14 @@ namespace Barotrauma "couldn't find an item prefab with the identifier " + kvp.Key); continue; } - var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.First, out float rotation); - if (spawnedResources.Count < kvp.Value.First) + var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.amount, out float rotation); + if (spawnedResources.Count < kvp.Value.amount) { DebugConsole.ThrowError("Error in MineralMission - " + - "spawned " + spawnedResources.Count + "/" + kvp.Value.First + " of " + prefab.Name); + "spawned " + spawnedResources.Count + "/" + kvp.Value.amount + " of " + prefab.Name); } if (spawnedResources.None()) { continue; } - SpawnedResources.Add(kvp.Key, spawnedResources); - kvp.Value.Second = rotation; + this.spawnedResources.Add(kvp.Key, spawnedResources); foreach (Level.Cave cave in Level.Loaded.Caves) { @@ -142,7 +141,7 @@ namespace Barotrauma GiveReward(); completed = true; } - foreach (var kvp in SpawnedResources) + foreach (var kvp in spawnedResources) { foreach (var i in kvp.Value) { @@ -152,33 +151,33 @@ namespace Barotrauma } } } - SpawnedResources.Clear(); - RelevantLevelResources.Clear(); - MissionClusterPositions.Clear(); + spawnedResources.Clear(); + relevantLevelResources.Clear(); + missionClusterPositions.Clear(); failed = !completed && state > 0; } private void FindRelevantLevelResources() { - RelevantLevelResources.Clear(); - foreach (var identifier in ResourceClusters.Keys) + relevantLevelResources.Clear(); + foreach (var identifier in resourceClusters.Keys) { var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && i.Submarine == null && i.ParentInventory == null && (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) .ToArray(); - RelevantLevelResources.Add(identifier, items); + relevantLevelResources.Add(identifier, items); } } private bool EnoughHaveBeenCollected() { - foreach (var kvp in ResourceClusters) + foreach (var kvp in resourceClusters) { - if (RelevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) + if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { var collected = availableResources.Count(r => HasBeenCollected(r)); - var needed = kvp.Value.First; + var needed = kvp.Value.amount; if (collected < needed) { return false; } } else @@ -210,8 +209,8 @@ namespace Barotrauma private void CalculateMissionClusterPositions() { - MissionClusterPositions.Clear(); - foreach (var kvp in SpawnedResources) + missionClusterPositions.Clear(); + foreach (var kvp in spawnedResources) { if (kvp.Value.None()) { continue; } var pos = Vector2.Zero; @@ -222,7 +221,7 @@ namespace Barotrauma itemCount++; } pos /= itemCount; - MissionClusterPositions.Add(new Tuple(kvp.Key, pos)); + missionClusterPositions.Add(new Tuple(kvp.Key, pos)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 77747d13a..507d7b61f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -1,4 +1,6 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Abilities; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; @@ -25,7 +27,7 @@ namespace Barotrauma state = value; TryTriggerEvents(state); #if SERVER - GameMain.Server?.UpdateMissionState(this, state); + GameMain.Server?.UpdateMissionState(this); #endif ShowMessage(State); } @@ -343,19 +345,57 @@ namespace Barotrauma public void GiveReward() { if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; } - campaign.Money += GetReward(Submarine.MainSub); + int reward = GetReward(Submarine.MainSub); + + float baseExperienceGain = reward * 0.09f; + + float difficultyMultiplier = 1 + level.Difficulty / 100f; + baseExperienceGain *= difficultyMultiplier; + + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); + + // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking + var experienceGainMultiplier = new AbilityValue(1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); + crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); + + int experienceGain = (int)(baseExperienceGain * experienceGainMultiplier.Value); +#if CLIENT + foreach (Character character in crewCharacters) + { + character.Info?.GiveExperience(experienceGain, isMissionExperience: true); + } +#else + foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) + { + //give the experience to the stored characterinfo if the client isn't currently controlling a character + (c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true); + } +#endif + + // apply money gains afterwards to prevent them from affecting XP gains + var moneyGainMission = new AbilityValueMission(1f, this); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission)); + crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + + campaign.Money += (int)(reward * moneyGainMission.Value); + + foreach (Character character in crewCharacters) + { + character.Info.MissionsCompletedSinceDeath++; + } foreach (KeyValuePair reputationReward in ReputationRewards) { if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) { - Locations[0].Reputation.Value += reputationReward.Value; - Locations[1].Reputation.Value += reputationReward.Value; + Locations[0].Reputation.AddReputation(reputationReward.Value); + Locations[1].Reputation.AddReputation(reputationReward.Value); } else { Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); - if (faction != null) { faction.Reputation.Value += reputationReward.Value; } + if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } } } @@ -443,5 +483,55 @@ namespace Barotrauma return spawnedCharacter; } + + protected ItemPrefab FindItemPrefab(XElement element) + { + ItemPrefab itemPrefab; + if (element.Attribute("name") != null) + { + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items"); + string itemName = element.GetAttributeString("name", ""); + itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found"); + } + } + else + { + string itemIdentifier = element.GetAttributeString("identifier", ""); + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found"); + } + } + return itemPrefab; + } + + protected Vector2? GetCargoSpawnPosition(ItemPrefab itemPrefab, out Submarine cargoRoomSub) + { + cargoRoomSub = null; + + WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); + if (cargoSpawnPos == null) + { + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found"); + return null; + } + + var cargoRoom = cargoSpawnPos.CurrentHull; + if (cargoRoom == null) + { + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room"); + return null; + } + + cargoRoomSub = cargoRoom.Submarine; + + return new Vector2( + cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), + cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 80be751bf..0fa150677 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -22,7 +22,9 @@ namespace Barotrauma Escort = 0x100, Pirate = 0x200, GoTo = 0x400, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo + ScanAlienRuins = 0x800, + ClearAlienRuins = 0x1000, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins } partial class MissionPrefab @@ -40,7 +42,9 @@ namespace Barotrauma { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) }, { MissionType.Escort, typeof(EscortMission) }, { MissionType.Pirate, typeof(PirateMission) }, - { MissionType.GoTo, typeof(GoToMission) } + { MissionType.GoTo, typeof(GoToMission) }, + { MissionType.ScanAlienRuins, typeof(ScanMission) }, + { MissionType.ClearAlienRuins, typeof(AlienRuinMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { @@ -372,6 +376,11 @@ namespace Barotrauma var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; } } + else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) + { + var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); + if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; } + } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 6d7319de6..e9d3cef40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -180,7 +180,11 @@ namespace Barotrauma var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos)); if (!path.Unreachable) { - preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible + var validNodes = path.Nodes.FindAll(n => !Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(n.WorldPosition)))); + if (validNodes.Any()) + { + preferredSpawnPos = validNodes.GetRandom().WorldPosition; // spawn the sub in a random point in the path if possible + } } int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead @@ -382,11 +386,11 @@ namespace Barotrauma State = newState; } - private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m))); + private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m)); - private bool Survived(Character character) + private bool DeadOrCaptured(Character character) { - return character != null && !character.Removed && !character.IsDead; + return character != null && !character.Removed && (character.IsDead || (character.LockHands && character.Submarine == Submarine.MainSub)); } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 36f611211..5f628660c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -126,12 +126,12 @@ namespace Barotrauma item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); break; case Level.PositionType.Ruin: - item = suitableItems.FirstOrDefault(it => it.ParentRuin != null && it.ParentRuin.Area.Contains(position)); - break; case Level.PositionType.Wreck: foreach (Item it in suitableItems) { - if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + if (it.Submarine?.Info == null) { continue; } + if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } + if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } Rectangle worldBorders = it.Submarine.Borders; worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); if (Submarine.RectContains(worldBorders, it.WorldPosition)) @@ -178,10 +178,10 @@ namespace Barotrauma { case Level.PositionType.Cave: case Level.PositionType.MainPath: - if (it.Submarine != null || it.ParentRuin != null) { continue; } + if (it.Submarine != null) { continue; } break; case Level.PositionType.Ruin: - if (it.ParentRuin == null) { continue; } + if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } break; case Level.PositionType.Wreck: if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } @@ -247,8 +247,8 @@ namespace Barotrauma public override void End() { - var root = item.GetRootContainer() ?? item; - if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndExit && !root.CurrentHull.Submarine.AtStartExit) || item.Removed) + var root = item?.GetRootContainer() ?? item; + if (root?.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndExit && !root.CurrentHull.Submarine.AtStartExit) || item.Removed) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..c84e43647 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -0,0 +1,286 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.RuinGeneration; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + private readonly XElement itemConfig; + private readonly List startingItems = new List(); + private readonly List scanners = new List(); + private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary parentItemContainerIndices = new Dictionary(); + private readonly int targetsToScan; + private readonly Dictionary scanTargets = new Dictionary(); + private readonly HashSet newTargetsScanned = new HashSet(); + private readonly float minTargetDistance; + + + private Ruin TargetRuin { get; set; } + + private bool AllTargetsScanned + { + get + { + return scanTargets.Any() && scanTargets.All(kvp => kvp.Value); + } + } + + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else if (scanTargets.Any()) + { + return scanTargets + .Where(kvp => !kvp.Value) + .Select(kvp => kvp.Key.WorldPosition); + } + else + { + return Enumerable.Empty(); + } + + } + } + + public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + itemConfig = prefab.ConfigElement.Element("Items"); + targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); + minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); + } + + protected override void StartMissionSpecific(Level level) + { + Reset(); + + if (IsClient) { return; } + + if (itemConfig == null) + { + DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set"); + return; + } + + foreach (var element in itemConfig.Elements()) + { + LoadItem(element, null); + } + GetScanners(); + + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + if (TargetRuin == null) + { + DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); + return; + } + + var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false); + ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); + if (ruinWaypoints.Count < targetsToScan) + { + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})"); + return; + } + var availableWaypoints = new List(); + float minTargetDistanceSquared = minTargetDistance * minTargetDistance; + for (int tries = 0; tries < 15; tries++) + { + scanTargets.Clear(); + availableWaypoints.Clear(); + availableWaypoints.AddRange(ruinWaypoints); + for (int i = 0; i < targetsToScan; i++) + { + var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.Server); + scanTargets.Add(selectedWaypoint, false); + availableWaypoints.Remove(selectedWaypoint); + if (i < (targetsToScan - 1)) + { + availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull); + availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared); + if (availableWaypoints.None()) + { +#if DEBUG + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); +#endif + break; + } + } + } + if (scanTargets.Count >= targetsToScan) + { +#if DEBUG + DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green); +#endif + break; + } + if ((tries + 1) % 5 == 0) + { + float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance; + minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance; +#if DEBUG + DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow); +#endif + } + } + if (scanTargets.Count < targetsToScan) + { + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + } + } + + private void Reset() + { + startingItems.Clear(); + parentInventoryIDs.Clear(); + parentItemContainerIndices.Clear(); + scanners.Clear(); + TargetRuin = null; + scanTargets.Clear(); + } + + private void LoadItem(XElement element, Item parent) + { + var itemPrefab = FindItemPrefab(element); + Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); + if (!position.HasValue) { return; } + var item = new Item(itemPrefab, position.Value, cargoRoomSub); + item.FindHull(); + startingItems.Add(item); + if (parent?.GetComponent() is ItemContainer itemContainer) + { + parentInventoryIDs.Add(item, parent.ID); + parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer)); + parent.Combine(item, user: null); + } + foreach (XElement subElement in element.Elements()) + { + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) + { + LoadItem(subElement, item); + } + } + } + + private void GetScanners() + { + foreach (var startingItem in startingItems) + { + if (startingItem.GetComponent() is Scanner scanner) + { + scanner.OnScanStarted += OnScanStarted; + if (!IsClient) + { + scanner.OnScanCompleted += OnScanCompleted; + } + scanners.Add(scanner); + } + } + } + + private void OnScanStarted(Scanner scanner) + { + float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; + foreach (var kvp in scanTargets) + { + if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } + scanner.DisplayProgressBar = true; + break; + } + } + + private void OnScanCompleted(Scanner scanner) + { + if (IsClient) { return; } + newTargetsScanned.Clear(); + float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; + foreach (var kvp in scanTargets) + { + if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } + newTargetsScanned.Add(kvp.Key); + } + foreach (var wp in newTargetsScanned) + { + scanTargets[wp] = true; + } +#if SERVER + // Server should make sure that the clients' scan target status is in-sync + GameMain.Server?.UpdateMissionState(this); +#endif + } + + private bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) + { + if (scanStatus.Value) { return false; } + if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; } + if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; } + return true; + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + if (IsClient) { return; } + switch (State) + { + case 0: + if (!AllTargetsScanned) { return; } + State = 1; + break; + case 1: + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + State = 2; + break; + } + } + + public override void End() + { + if (State == 2 && AllScannersReturned()) + { + GiveReward(); + completed = true; + } + foreach (var scanner in scanners) + { + if (scanner.Item != null && !scanner.Item.Removed) + { + scanner.OnScanStarted -= OnScanStarted; + scanner.OnScanCompleted -= OnScanCompleted; + scanner.Item.Remove(); + } + } + Reset(); + failed = !completed && state > 0; + + bool AllScannersReturned() + { + foreach (var scanner in scanners) + { + if (scanner?.Item == null || scanner.Item.Removed) { return false; } + var owner = scanner.Item.GetRootInventoryOwner(); + if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player) + { + continue; + } + else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info)) + { + continue; + } + return false; + } + return true; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 3edb99bdd..4cfd77e2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -21,10 +21,11 @@ namespace Barotrauma private bool disallowed; private readonly Level.PositionType spawnPosType; + private readonly string spawnPointTag; private bool spawnPending; - private int maxAmountPerLevel = int.MaxValue; + private readonly int maxAmountPerLevel = int.MaxValue; public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; @@ -87,6 +88,8 @@ namespace Barotrauma spawnPosType = Level.PositionType.Abyss; } + spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty); + offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); @@ -285,19 +288,19 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, ruin: chosenPosition.Ruin, useSyncedRand: false); - if (spawnPoint != null) + var spawnPoint = + WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag); + if (spawnPoint != null) { - System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); - System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin); - spawnPos = spawnPoint.WorldPosition; + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine)); + spawnPos = spawnPoint.WorldPosition; } else - { + { //no suitable position found, disable the event spawnPos = null; Finished(); - return; + return; } } else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath) @@ -448,7 +451,7 @@ namespace Barotrauma for (int i = 0; i < amount; i++) { string seed = Level.Loaded.Seed + i.ToString(); - CoroutineManager.InvokeAfter(() => + CoroutineManager.Invoke(() => { //round ended before the coroutine finished if (GameMain.GameSession == null || Level.Loaded == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs similarity index 69% rename from Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs rename to Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs index 3de8f949c..89fc4cefa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using System; namespace Barotrauma.Extensions { @@ -12,6 +11,11 @@ namespace Barotrauma.Extensions new Color((byte)(color.R * value), (byte)(color.G * value), (byte)(color.B * value), (byte)(color.A * value)); } + public static Color Multiply(this Color thisColor, Color color) + { + return new Color((byte)(thisColor.R * color.R / 255f), (byte)(thisColor.G * color.G / 255f), (byte)(thisColor.B * color.B / 255f), (byte)(thisColor.A * color.A / 255f)); + } + public static Color Opaque(this Color color) { return new Color(color.R, color.G, color.B, (byte)255); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs index 479b031d3..25c6dba97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/VectorExtensions.cs @@ -92,5 +92,12 @@ namespace Barotrauma.Extensions { return MathUtils.NearlyEqual(v.X, other.X) && MathUtils.NearlyEqual(v.Y, other.Y); } + + public static Vector2 Pad(this Vector2 v, Vector4 padding) + { + v.X += padding.X + padding.Z; + v.Y += padding.Y + padding.W; + return v; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index c93b28077..8f0c37464 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -8,14 +8,17 @@ namespace Barotrauma { static class AutoItemPlacer { - private static readonly List spawnedItems = new List(); - public static bool OutputDebugInfo = false; + /// + /// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle" + /// + public const float DefaultDifficultyModifier = 0f; + public static void PlaceIfNeeded() { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } - + for (int i = 0; i < Submarine.MainSubs.Length; i++) { if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; } @@ -24,14 +27,18 @@ namespace Barotrauma Place(subs); subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); } - + + float difficultyModifier = GetLevelDifficultyModifier(); foreach (var sub in Submarine.Loaded) { - if (sub.Info.Type == SubmarineType.Wreck || - sub.Info.Type == SubmarineType.BeaconStation) + if (sub.Info.Type == SubmarineType.Player || + sub.Info.Type == SubmarineType.Outpost || + sub.Info.Type == SubmarineType.OutpostModule || + sub.Info.Type == SubmarineType.EnemySubmarine) { - Place(sub.ToEnumerable()); + continue; } + Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier); } if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost) @@ -41,7 +48,23 @@ namespace Barotrauma } } - private static void Place(IEnumerable subs) + private const float MaxDifficultyModifier = 0.2f; + + /// + /// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty. + /// + private static float GetLevelDifficultyModifier() + { + return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier); + } + + public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) + { + // Level difficulty currently doesn't affect regenerated loot for the sake of simplicity + Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer); + } + + private static void Place(IEnumerable subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { @@ -49,19 +72,29 @@ namespace Barotrauma return; } + List spawnedItems = new List(100); + int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); var prefabsWithContainer = new List(itemCountApprox / 3); var prefabsWithoutContainer = new List(itemCountApprox); var removals = new List(); - foreach (Item item in Item.ItemList) + // generate loot only for a specific container if defined + if (regeneratedContainer != null) { - if (!subs.Contains(item.Submarine)) { continue; } - if (item.GetRootInventoryOwner() is Character) { continue; } - containers.AddRange(item.GetComponents()); + containers.Add(regeneratedContainer); + } + else + { + foreach (Item item in Item.ItemList) + { + if (!subs.Contains(item.Submarine)) { continue; } + if (item.GetRootInventoryOwner() is Character) { continue; } + containers.AddRange(item.GetComponents()); + } + containers.Shuffle(Rand.RandSync.Server); } - containers.Shuffle(Rand.RandSync.Server); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { @@ -77,7 +110,6 @@ namespace Barotrauma } } - spawnedItems.Clear(); var validContainers = new Dictionary(); prefabsWithContainer.Shuffle(Rand.RandSync.Server); // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) @@ -152,8 +184,10 @@ namespace Barotrauma } foreach (var validContainer in validContainers) { - if (SpawnItem(itemPrefab, containers, validContainer)) + var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier); + if (newItems.Any()) { + spawnedItems.AddRange(newItems); success = true; } } @@ -184,14 +218,22 @@ namespace Barotrauma return validContainers; } - private static bool SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) + private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1] { - bool success = false; - if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return false; } + (0, 0.85f), + (1, 0.125f), + (2, 0.0225f), + (3, 0.0025f), + }; + + private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) + { + List spawnedItems = new List(); + if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { - return false; + return spawnedItems; } int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server); for (int i = 0; i < amount; i++) @@ -201,11 +243,20 @@ namespace Barotrauma containers.Remove(validContainer.Key); break; } - if (!validContainer.Key.Inventory.CanBePut(itemPrefab)) { break; } + + var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + int quality = + existingItem?.Quality ?? + ToolBox.SelectWeightedRandom( + qualityCommonnesses.Select(q => q.quality).ToList(), + qualityCommonnesses.Select(q => q.commonness).ToList(), + Rand.RandSync.Server); + if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine) { SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, + Quality = quality, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, OriginalContainerIndex = Item.ItemList.Where(it => it.Submarine == validContainer.Key.Item.Submarine && it.OriginalModuleIndex == validContainer.Key.Item.OriginalModuleIndex).ToList().IndexOf(validContainer.Key.Item) @@ -217,9 +268,8 @@ namespace Barotrauma spawnedItems.Add(item); validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false); containers.AddRange(item.GetComponents()); - success = true; } - return success; + return spawnedItems; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 8eed7fcfa..5aa20f202 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -237,6 +237,9 @@ namespace Barotrauma { CharacterInfo.ApplyHealthData(character, character.Info.HealthData); } + + character.LoadTalents(); + character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) @@ -486,6 +489,11 @@ namespace Barotrauma continue; } } + if (orderInfo.Order.TargetEntity == null || (orderInfo.Order.IsIgnoreOrder && ignoreTarget == null)) + { + // The order target doesn't exist anymore, just discard the loaded order + continue; + } if (ignoreTarget != null) { ignoreTarget.OrderedToBeIgnored = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 9944fd2bc..85a3ef2f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { @@ -31,7 +32,7 @@ namespace Barotrauma public float Value { get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation)); - set + private set { if (MathUtils.NearlyEqual(Value, value)) { return; } Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation)); @@ -40,6 +41,25 @@ namespace Barotrauma } } + public void SetReputation(float newReputation) + { + Value = newReputation; + } + + public void AddReputation(float reputationChange) + { + if (reputationChange > 0f) + { + float reputationGainMultiplier = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters()) + { + reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + } + reputationChange *= reputationGainMultiplier; + } + Value += reputationChange; + } + public Action OnReputationValueChanged; public static Action OnAnyReputationValueChanged; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 9c7e037e4..a1dfda365 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -1,12 +1,11 @@ using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; -using Barotrauma.Extensions; namespace Barotrauma { @@ -17,7 +16,15 @@ namespace Barotrauma // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down public static CampaignSettings Unsure = Empty; public bool RadiationEnabled { get; set; } - public int MaxMissionCount { get; set; } + + public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount(); + + private int maxMissionCount; + public int MaxMissionCount + { + get { return maxMissionCount; } + set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); } + } public const int DefaultMaxMissionCount = 2; public const int MaxMissionCountLimit = 10; @@ -25,14 +32,16 @@ namespace Barotrauma public CampaignSettings(IReadMessage inc) { + maxMissionCount = DefaultMaxMissionCount; RadiationEnabled = inc.ReadBoolean(); MaxMissionCount = inc.ReadInt32(); } - + public CampaignSettings(XElement element) { - RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true); - MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLower(), DefaultMaxMissionCount); + maxMissionCount = DefaultMaxMissionCount; + RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true); + MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount); } public void Serialize(IWriteMessage msg) @@ -41,9 +50,19 @@ namespace Barotrauma msg.Write(MaxMissionCount); } + public int GetAddedMissionCount() + { + int count = 0; + foreach (Character character in GameSession.GetSessionCrewCharacters()) + { + count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); + } + return count; + } + public XElement Save() { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLower().ToLower(), MaxMissionCount)); + return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount)); } } @@ -226,6 +245,8 @@ namespace Barotrauma PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); + + ResetTalentData(); } public void InitCampaignData() @@ -522,6 +543,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null) { Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else @@ -546,13 +568,16 @@ namespace Barotrauma public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { List takenItems = new List(); - foreach (Item item in Item.ItemList) + if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { - if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; } - var owner = item.GetRootInventoryOwner(); - if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost) + foreach (Item item in Item.ItemList) { - takenItems.Add(item); + if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; } + var owner = item.GetRootInventoryOwner(); + if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost) + { + takenItems.Add(item); + } } } if (map != null && CargoManager != null) @@ -642,16 +667,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - if (location.Type != location.OriginalType) - { - location.ChangeType(location.OriginalType); - location.PendingLocationTypeChange = null; - } - location.CreateStore(force: true); - location.ClearMissions(); - location.Discovered = false; - location.LevelData?.EventHistory?.Clear(); - location.UnlockInitialMissions(); + location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); @@ -846,7 +862,7 @@ namespace Barotrauma Location location = Map?.CurrentLocation; if (location != null) { - location.Reputation.Value -= attackResult.Damage * Reputation.ReputationLossPerNPCDamage; + location.Reputation.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); } } @@ -898,15 +914,24 @@ namespace Barotrauma { foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation))) { - if (NumberOfMissionsAtLocation(location) > Settings.MaxMissionCount) + if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount) { DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); - foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.MaxMissionCount).ToList()) + foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList()) { currentLocation.DeselectMission(mission); } } } } + + // Talent relevant data, only stored for the duration of the mission + private void ResetTalentData() + { + CrewHasDied = false; + } + + public bool CrewHasDied { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs index 163255fdd..0df3b6f0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,6 +1,4 @@ -using Barotrauma.Networking; -using System.Globalization; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -29,65 +27,6 @@ namespace Barotrauma private XElement healthData; public XElement OrderData { get; private set; } - partial void InitProjSpecific(Client client); - public CharacterCampaignData(Client client, bool giveRespawnPenaltyAffliction = false) - { - Name = client.Name; - InitProjSpecific(client); - - healthData = new XElement("health"); - client.Character?.CharacterHealth?.Save(healthData); - if (giveRespawnPenaltyAffliction) - { - var respawnPenaltyAffliction = RespawnManager.GetRespawnPenaltyAffliction(); - healthData.Add(new XElement("Affliction", - new XAttribute("identifier", respawnPenaltyAffliction.Identifier), - new XAttribute("strength", respawnPenaltyAffliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); - } - if (client.Character?.Inventory != null) - { - itemData = new XElement("inventory"); - Character.SaveInventory(client.Character.Inventory, itemData); - } - OrderData = new XElement("orders"); - if (client.Character != null) - { - CharacterInfo.SaveOrderData(client.Character.Info, OrderData); - } - } - - public CharacterCampaignData(XElement element) - { - Name = element.GetAttributeString("name", "Unnamed"); - ClientEndPoint = element.GetAttributeString("endpoint", null) ?? element.GetAttributeString("ip", ""); - string steamID = element.GetAttributeString("steamid", ""); - if (!string.IsNullOrEmpty(steamID)) - { - ulong.TryParse(steamID, out ulong parsedID); - SteamID = parsedID; - } - - foreach (XElement subElement in element.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "character": - case "characterinfo": - CharacterInfo = new CharacterInfo(subElement); - break; - case "inventory": - itemData = subElement; - break; - case "health": - healthData = subElement; - break; - case "orders": - OrderData = subElement; - break; - } - } - } - public void Refresh(Character character) { healthData = new XElement("health"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index b5bb43eca..739f6f112 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -173,6 +173,12 @@ namespace Barotrauma if (matchingSub != null) { availableSubs.Add(matchingSub); } } break; + case "savedexperiencepoints": + foreach (XElement savedExp in subElement.Elements()) + { + savedExperiencePoints.Add(new SavedExperiencePoints(savedExp)); + } + break; #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index b17a7ec74..9523e5299 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -29,6 +29,8 @@ namespace Barotrauma public bool IsRunning { get; private set; } + public bool RoundEnding { get; private set; } + public Level Level { get; private set; } public LevelData LevelData { get; private set; } @@ -450,9 +452,11 @@ namespace Barotrauma StatusEffect.StopAll(); #if CLIENT +#if !DEBUG GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; +#endif if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } - if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode; + if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameMain.Config.LosMode; } #endif LevelData = level?.LevelData; Level = level; @@ -654,43 +658,89 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime); + public static IEnumerable GetSessionCrewCharacters() + { +#if SERVER + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); +#else + if (GameMain.GameSession == null) { return Enumerable.Empty(); } + return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead); +#endif + } + public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { - foreach (Mission mission in missions) - { - mission.End(); - } -#if CLIENT - if (GUI.PauseMenuOpen) - { - GUI.TogglePauseMenu(); - } - GUI.PreventPauseMenuToggle = true; + RoundEnding = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + try { - GUI.ClearMessages(); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); - GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); - GUIMessageBox.MessageBoxes.Add(summaryFrame); - RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; - } + IEnumerable crewCharacters = GetSessionCrewCharacters(); - if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded(); - TabMenu.OnRoundEnded(); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); -#endif - SteamAchievementManager.OnRoundEnded(this); + foreach (Mission mission in missions) + { + mission.End(); + } - GameMode?.End(transitionType); - EventManager?.EndRound(); - StatusEffect.StopAll(); - missions.Clear(); - IsRunning = false; + foreach (Character character in crewCharacters) + { + character.CheckTalents(AbilityEffectType.OnRoundEnd); + } + + if (missions.Any()) + { + if (missions.Any(m => m.Completed)) + { + foreach (Character character in crewCharacters) + { + character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); + } + } + + if (missions.All(m => m.Completed)) + { + foreach (Character character in crewCharacters) + { + character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted); + } + } + } #if CLIENT - HintManager.OnRoundEnded(); + if (GUI.PauseMenuOpen) + { + GUI.TogglePauseMenu(); + } + GUI.PreventPauseMenuToggle = true; + + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + { + GUI.ClearMessages(); + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); + GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, traitorResults, transitionType); + GUIMessageBox.MessageBoxes.Add(summaryFrame); + RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }; + } + + if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); } + TabMenu.OnRoundEnded(); + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); #endif + SteamAchievementManager.OnRoundEnded(this); + + GameMode?.End(transitionType); + EventManager?.EndRound(); + StatusEffect.StopAll(); + missions.Clear(); + IsRunning = false; + +#if CLIENT + HintManager.OnRoundEnded(); +#endif + } + finally + { + RoundEnding = false; + } } public void KillCharacter(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index d5cc99aea..b8b37bcc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -143,14 +143,7 @@ namespace Barotrauma return true; } - public int CharacterHeadIndex { get; set; } - public int CharacterHairIndex { get; set; } - public int CharacterBeardIndex { get; set; } - public int CharacterMoustacheIndex { get; set; } - public int CharacterFaceAttachmentIndex { get; set; } - - public Gender CharacterGender { get; set; } - public Race CharacterRace { get; set; } + internal CharacterInfo.HeadInfo PlayerCharacterCustomization { get; set; } private float aimAssistAmount; public float AimAssistAmount @@ -307,6 +300,7 @@ namespace Barotrauma public bool AutomaticQuickStartEnabled { get; set; } public bool AutomaticCampaignLoadEnabled { get; set; } public bool TextManagerDebugModeEnabled { get; set; } + public bool TestScreenEnabled { get; set; } public bool ModBreakerMode { get; set; } #endif @@ -548,6 +542,12 @@ namespace Barotrauma case ContentType.Text: TextManager.LoadTextPack(file.Path); break; + case ContentType.Talents: + TalentPrefab.LoadFromFile(file); + break; + case ContentType.TalentTrees: + TalentTree.LoadFromFile(file); + break; #if CLIENT case ContentType.Particles: GameMain.ParticleManager?.LoadPrefabsFromFile(file); @@ -594,6 +594,12 @@ namespace Barotrauma case ContentType.Text: TextManager.RemoveTextPack(file.Path); break; + case ContentType.Talents: + TalentPrefab.LoadFromFile(file); + break; + case ContentType.TalentTrees: + TalentTree.LoadFromFile(file); + break; #if CLIENT case ContentType.Particles: GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); @@ -703,7 +709,6 @@ namespace Barotrauma public string MasterServerUrl { get; set; } public string RemoteContentUrl { get; set; } public bool AutoCheckUpdates { get; set; } - public bool WasGameUpdated { get; set; } private string playerName; public string PlayerName @@ -796,13 +801,6 @@ namespace Barotrauma LoadDefaultConfig(); - if (WasGameUpdated) - { - UpdaterUtil.CleanOldFiles(); - WasGameUpdated = false; - SaveNewDefaultConfig(); - } - LoadPlayerConfig(); } @@ -827,7 +825,6 @@ namespace Barotrauma MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", MasterServerUrl); RemoteContentUrl = doc.Root.GetAttributeString("remotecontenturl", RemoteContentUrl); - WasGameUpdated = doc.Root.GetAttributeBool("wasgameupdated", WasGameUpdated); VerboseLogging = doc.Root.GetAttributeBool("verboselogging", VerboseLogging); SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", SaveDebugConsoleLogs); AutoUpdateWorkshopItems = doc.Root.GetAttributeBool("autoupdateworkshopitems", AutoUpdateWorkshopItems); @@ -851,176 +848,6 @@ namespace Barotrauma UnsavedSettings = false; } - private void SaveNewDefaultConfig() - { - XDocument doc = new XDocument(); - - if (doc.Root == null) - { - doc.Add(new XElement("config")); - } - - doc.Root.Add( - new XAttribute("language", TextManager.Language), - new XAttribute("masterserverurl", MasterServerUrl), - new XAttribute("remotecontenturl", RemoteContentUrl), - new XAttribute("autocheckupdates", AutoCheckUpdates), - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("microphonevolume", microphoneVolume), - new XAttribute("voicechatvolume", voiceChatVolume), - new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), - new XAttribute("verboselogging", VerboseLogging), - new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), - new XAttribute("submarineautosave", EnableSubmarineAutoSave), - new XAttribute("maxautosaves", MaximumAutoSaves), - new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), - new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), - new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), - new XAttribute("enablesplashscreen", EnableSplashScreen), - new XAttribute("usesteammatchmaking", UseSteamMatchmaking), - new XAttribute("quickstartsub", QuickStartSubmarineName), - new XAttribute("requiresteamauthentication", RequireSteamAuthentication), - new XAttribute("aimassistamount", aimAssistAmount), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning)); - - if (!ShowUserStatisticsPrompt) - { - doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics)); - } - - if (WasGameUpdated) - { - doc.Root.Add(new XAttribute("wasgameupdated", true)); - } - - XElement gMode = doc.Root.Element("graphicsmode"); - if (gMode == null) - { - gMode = new XElement("graphicsmode"); - doc.Root.Add(gMode); - } - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode)); - } - else - { - gMode.ReplaceAttributes( - new XAttribute("width", GraphicsWidth), - new XAttribute("height", GraphicsHeight), - new XAttribute("vsync", VSyncEnabled), - new XAttribute("framelimit", Timing.FrameLimit), - new XAttribute("displaymode", windowMode)); - } - - XElement gSettings = doc.Root.Element("graphicssettings"); - if (gSettings == null) - { - gSettings = new XElement("graphicssettings"); - doc.Root.Add(gSettings); - } - - gSettings.ReplaceAttributes( - new XAttribute("particlelimit", ParticleLimit), - new XAttribute("lightmapscale", LightMapScale), - new XAttribute("chromaticaberration", ChromaticAberrationEnabled), - new XAttribute("losmode", LosMode), - new XAttribute("hudscale", HUDScale), - new XAttribute("inventoryscale", InventoryScale)); - - foreach (ContentPackage contentPackage in ContentPackage.CorePackages) - { - if (contentPackage.Path.Contains(VanillaContentPackagePath)) - { - doc.Root.Add(new XElement("contentpackages", new XElement("core", new XAttribute("name", contentPackage.Name)))); - break; - } - } - -#if CLIENT - var keyMappingElement = new XElement("keymapping"); - doc.Root.Add(keyMappingElement); - for (int i = 0; i < keyMapping.Length; i++) - { - KeyOrMouse bind = keyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.Key)); - } - else - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.MouseButton)); - } - } - - var inventoryKeyMappingElement = new XElement("inventorykeymapping"); - doc.Root.Add(inventoryKeyMappingElement); - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - KeyOrMouse bind = inventoryKeyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); - } - else - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); - } - } -#endif - - var gameplay = new XElement("gameplay"); - var jobPreferences = new XElement("jobpreferences"); - foreach (Pair job in JobPreferences) - { - XElement jobElement = new XElement("job"); - jobElement.Add(new XAttribute("identifier", job.First)); - jobElement.Add(new XAttribute("variant", job.Second)); - jobPreferences.Add(jobElement); - } - gameplay.Add(jobPreferences); - - var teamPreference = new XElement("teampreference"); - teamPreference.Add(new XAttribute("team", TeamPreference.ToString())); - gameplay.Add(teamPreference); - - doc.Root.Add(gameplay); - - var playerElement = new XElement("player", - new XAttribute("name", playerName ?? ""), - new XAttribute("headindex", CharacterHeadIndex), - new XAttribute("gender", CharacterGender), - new XAttribute("race", CharacterRace), - new XAttribute("hairindex", CharacterHairIndex), - new XAttribute("beardindex", CharacterBeardIndex), - new XAttribute("moustacheindex", CharacterMoustacheIndex), - new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); - doc.Root.Add(playerElement); - - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings - { - Indent = true, - OmitXmlDeclaration = true, - NewLineOnAttributes = true - }; - - try - { - using (var writer = XmlWriter.Create(SavePath, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving game settings failed.", e); - GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - } - } - #region Load PlayerConfig public void LoadPlayerConfig() { @@ -1115,6 +942,7 @@ namespace Barotrauma } doc.Root.Add( + new XAttribute("gameversion", GameMain.Version.ToString()), new XAttribute("language", TextManager.Language), new XAttribute("masterserverurl", MasterServerUrl), new XAttribute("autocheckupdates", AutoCheckUpdates), @@ -1147,6 +975,7 @@ namespace Barotrauma new XAttribute("disableingamehints", DisableInGameHints) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) + , new XAttribute(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled) , new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled) , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) , new XAttribute("modbreakermode", ModBreakerMode) @@ -1307,15 +1136,20 @@ namespace Barotrauma gameplay.Add(jobPreferences); doc.Root.Add(gameplay); - var playerElement = new XElement("player", - new XAttribute("name", playerName ?? ""), - new XAttribute("headindex", CharacterHeadIndex), - new XAttribute("gender", CharacterGender), - new XAttribute("race", CharacterRace), - new XAttribute("hairindex", CharacterHairIndex), - new XAttribute("beardindex", CharacterBeardIndex), - new XAttribute("moustacheindex", CharacterMoustacheIndex), - new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); + var playerElement = new XElement("player", new XAttribute("name", playerName ?? "")); + if (PlayerCharacterCustomization != null) + { + playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId); + if (PlayerCharacterCustomization.gender != Gender.None) { playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); } + if (PlayerCharacterCustomization.race != Race.None) { playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); } + playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex); + playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex); + playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex); + playerElement.SetAttributeValue("faceattachmentindex", PlayerCharacterCustomization.FaceAttachmentIndex); + playerElement.SetAttributeValue("skincolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.SkinColor)); + playerElement.SetAttributeValue("haircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.HairColor)); + playerElement.SetAttributeValue("facialhaircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.FacialHairColor)); + } doc.Root.Add(playerElement); #if CLIENT @@ -1402,6 +1236,7 @@ namespace Barotrauma DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); + TestScreenEnabled = doc.Root.GetAttributeBool(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled); AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled); TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode); @@ -1433,23 +1268,22 @@ namespace Barotrauma if (playerElement != null) { playerName = playerElement.GetAttributeString("name", playerName); - CharacterHeadIndex = playerElement.GetAttributeInt("headindex", CharacterHeadIndex); - if (Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender g)) + int head = playerElement.GetAttributeInt("headindex", -1); + Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender gender); + Enum.TryParse(playerElement.GetAttributeString("race", "none"), true, out Race race); + int hair = playerElement.GetAttributeInt("hairindex", -1); + int beard = playerElement.GetAttributeInt("beardindex", -1); + int moustache = playerElement.GetAttributeInt("moustacheindex", -1); + int faceAttachment = playerElement.GetAttributeInt("faceattachmentindex", -1); + Color skinColor = playerElement.GetAttributeColor("skincolor", Color.Black); + Color hairColor = playerElement.GetAttributeColor("haircolor", Color.Black); + Color facialHairColor = playerElement.GetAttributeColor("facialhaircolor", Color.Black); + PlayerCharacterCustomization = new CharacterInfo.HeadInfo(head, gender, race, hair, beard, moustache, faceAttachment) { - CharacterGender = g; - } - if (Enum.TryParse(playerElement.GetAttributeString("race", "white"), true, out Race r)) - { - CharacterRace = r; - } - else - { - CharacterRace = Race.White; - } - CharacterHairIndex = playerElement.GetAttributeInt("hairindex", CharacterHairIndex); - CharacterBeardIndex = playerElement.GetAttributeInt("beardindex", CharacterBeardIndex); - CharacterMoustacheIndex = playerElement.GetAttributeInt("moustacheindex", CharacterMoustacheIndex); - CharacterFaceAttachmentIndex = playerElement.GetAttributeInt("faceattachmentindex", CharacterFaceAttachmentIndex); + SkinColor = skinColor, + HairColor = hairColor, + FacialHairColor = facialHairColor + }; } } @@ -1655,13 +1489,7 @@ namespace Barotrauma UseSteamMatchmaking = true; RequireSteamAuthentication = true; QuickStartSubmarineName = string.Empty; - CharacterHeadIndex = 1; - CharacterHairIndex = -1; - CharacterBeardIndex = -1; - CharacterMoustacheIndex = -1; - CharacterFaceAttachmentIndex = -1; - CharacterGender = Gender.None; - CharacterRace = Race.White; + PlayerCharacterCustomization = null; aimAssistAmount = 0.5f; EnableMouseLook = true; EnableRadialDistortion = true; @@ -1686,7 +1514,6 @@ namespace Barotrauma Language = "English"; } MasterServerUrl = "http://www.undertowgames.com/baromaster"; - WasGameUpdated = false; VerboseLogging = false; SaveDebugConsoleLogs = false; AutoUpdateWorkshopItems = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 49bfdb3e8..0aa9fc4fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -9,7 +9,7 @@ namespace Barotrauma [Flags] public enum InvSlotType { - None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256 + None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256, HealthInterface = 512 }; partial class CharacterInventory : Inventory @@ -87,7 +87,9 @@ namespace Barotrauma continue; } - Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false)); + string slotString = subElement.GetAttributeString("slot", "None"); + InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; + Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot); } } @@ -139,10 +141,10 @@ namespace Barotrauma (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); } - public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition) + public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { return - base.CanBePutInSlot(itemPrefab, i, condition) && + base.CanBePutInSlot(itemPrefab, i, condition, quality) && (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); } @@ -291,11 +293,25 @@ namespace Barotrauma { currentSlot = i; if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) - inSuitableSlot = true; + { + if ((SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) && !allowedSlots.Contains(SlotTypes[i])) + { + //allowed slot = InvSlotType.RightHand | InvSlotType.LeftHand + // -> make sure the item is in both hand slots + inSuitableSlot = IsInLimbSlot(item, InvSlotType.RightHand) && IsInLimbSlot(item, InvSlotType.LeftHand); + } + else + { + inSuitableSlot = true; + } + } else if (!allowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) + { inWrongSlot = true; + } } } + //all good if (inSuitableSlot && !inWrongSlot) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index a5e681c9a..e2d433d8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -204,8 +204,8 @@ namespace Barotrauma.Items.Components target.InitializeLinks(); - if (!item.linkedTo.Contains(target.item)) item.linkedTo.Add(target.item); - if (!target.item.linkedTo.Contains(item)) target.item.linkedTo.Add(item); + if (!item.linkedTo.Contains(target.item)) { item.linkedTo.Add(target.item); } + if (!target.item.linkedTo.Contains(item)) { target.item.linkedTo.Add(item); } if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target); if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this); @@ -291,7 +291,7 @@ namespace Barotrauma.Items.Components List removedEntities = item.linkedTo.Where(e => e.Removed).ToList(); - foreach (MapEntity removed in removedEntities) item.linkedTo.Remove(removed); + foreach (MapEntity removed in removedEntities) { item.linkedTo.Remove(removed); } if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull)) { @@ -306,9 +306,8 @@ namespace Barotrauma.Items.Components if (myWayPoint != null && targetWayPoint != null) { myWayPoint.FindHull(); - myWayPoint.linkedTo.Add(targetWayPoint); targetWayPoint.FindHull(); - targetWayPoint.linkedTo.Add(myWayPoint); + myWayPoint.ConnectTo(targetWayPoint); } } } @@ -597,8 +596,9 @@ namespace Barotrauma.Items.Components { hullRects[i].X -= expand; hullRects[i].Width += expand * 2; - hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); + hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -716,8 +716,9 @@ namespace Barotrauma.Items.Components { hullRects[i].Y += expand; hullRects[i].Height += expand * 2; - hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); + hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); + hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"; hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); @@ -873,8 +874,10 @@ namespace Barotrauma.Items.Components { myWayPoint.FindHull(); myWayPoint.linkedTo.Remove(targetWayPoint); + myWayPoint.OnLinksChanged?.Invoke(myWayPoint); targetWayPoint.FindHull(); targetWayPoint.linkedTo.Remove(myWayPoint); + targetWayPoint.OnLinksChanged?.Invoke(targetWayPoint); } } @@ -1058,7 +1061,7 @@ namespace Barotrauma.Items.Components } } - if (!item.linkedTo.Any()) return; + if (!item.linkedTo.Any()) { return; } List linked = new List(item.linkedTo); foreach (MapEntity entity in linked) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 99927ca74..bf6875202 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -67,7 +67,7 @@ namespace Barotrauma.Items.Components private bool isBroken; - public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck; + public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable; public bool IsBroken { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index a86e3f923..279fa113d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -184,6 +184,7 @@ namespace Barotrauma.Items.Components { foreach ((Character character, Node node) in charactersInRange) { + if (character == null || character.Removed) { continue; } character.ApplyAttack(null, node.WorldPosition, attack, 1.0f); } } @@ -475,6 +476,21 @@ namespace Barotrauma.Items.Components } } + public override void ReceiveSignal(Signal signal, Connection connection) + { + switch (connection.Name) + { + case "activate": + case "use": + case "trigger_in": + if (signal.value != "0") + { + item.Use(1.0f, null); + } + break; + } + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs new file mode 100644 index 000000000..e73ea86f1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -0,0 +1,290 @@ +#nullable enable + +using System; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent + { + public enum AreaShape + { + Rectangle, + Circle + } + + [Editable, Serialize("", true, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")] + public string? ItemIdentifier { get; set; } + + [Editable, Serialize("", true, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")] + public string? SpeciesName { get; set; } + + [Editable, Serialize(true, true, "Only spawn if crew members are within certain area")] + public bool OnlySpawnWhenCrewInRange { get; set; } + + [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where crew members need to stay")] + public AreaShape CrewAreaShape { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")] + public Vector2 CrewAreaBounds { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")] + public float CrewAreaRadius { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the crew area from the center of the item")] + public Vector2 CrewAreaOffset { get; set; } + + [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where enemies or items are spawned")] + public AreaShape SpawnAreaShape { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")] + public Vector2 SpawnAreaBounds { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")] + public float SpawnAreaRadius { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the spawn area from the center of the item")] + public Vector2 SpawnAreaOffset { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", true, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")] + public Vector2 SpawnTimerRange { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")] + public Vector2 SpawnAmountRange { get; set; } + + [Editable(MinValueInt = int.MinValue, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")] + public int MaximumAmount { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] + public float MaximumAmountRangePadding { get; set; } + + [Serialize(true, true, "")] + public bool CanSpawn { get; set; } = true; + + private float SpawnTimer; + private float? SpawnTimerGoal; + + public EntitySpawnerComponent(Item item, XElement element) : base(item, element) + { + IsActive = true; + } + + public override void OnItemLoaded() + { + if (!string.IsNullOrWhiteSpace(ItemIdentifier)) + { + string[] allItems = ItemIdentifier.Split(','); + foreach (string itemIdentifier in allItems) + { + string trimmedString = itemIdentifier.Trim(); + + bool found = false; + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs) + { + if (string.Equals(trimmedString, prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + + if (!found) + { + DebugConsole.ThrowError($"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name + "\" (identifier \"" + trimmedString + "\") not found."); + } + } + } + + base.OnItemLoaded(); + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + + item.SendSignal(CanSpawn ? "1" : "0", "state_out"); + + if (GameMain.NetworkMember is { IsClient: true }) { return; } + + float minTime = Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y), + maxTime = Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y); + + if (minTime < 0 && maxTime < 0) { return; } + + SpawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced); + + SpawnTimer += deltaTime; + + if (SpawnTimer > SpawnTimerGoal) + { + Spawn(); + SpawnTimerGoal = null; + SpawnTimer = 0; + } + } + + public override void ReceiveSignal(Signal signal, Connection connection) + { + bool isNonZero = signal.value != "0"; + bool isClient = GameMain.NetworkMember is { IsClient: true }; + + switch (connection.Name) + { + case "set_state": + CanSpawn = isNonZero; + break; + case "toggle" when isNonZero: + CanSpawn = !CanSpawn; + break; + case "trigger_in" when isNonZero && !isClient: + Spawn(); + break; + } + } + + private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw) + { + Vector2 pos = item.WorldPosition; + if (draw) + { + pos.Y = -pos.Y; + } + + pos += offset; + RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y); + return rect; + } + + private bool CanSpawnMore() + { + if (!CanSpawn) { return false; } + + if (OnlySpawnWhenCrewInRange) + { + if (!Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea: true, rangePad: false))) + { + return false; + } + } + + if (MaximumAmount < 0) { return true; } + + int amount; + + if (!string.IsNullOrWhiteSpace(SpeciesName)) + { + amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); + } + else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) + { + amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.OrdinalIgnoreCase) && IsInRange(it.WorldPosition, crewArea: false, rangePad: true)); + } + else + { + return false; + } + + return amount < MaximumAmount; + } + + private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false) + { + Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset; + offset.Y = -offset.Y; + switch (crewArea ? CrewAreaShape : SpawnAreaShape) + { + case AreaShape.Circle: + Vector2 center = item.WorldPosition + offset; + float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0); + return Vector2.DistanceSquared(worldPos, center) < distance * distance; + + case AreaShape.Rectangle: + RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw: false); + if (rangePad) + { + rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding); + } + + return rect.Contains(worldPos); + } + + return false; + } + + public void Spawn() + { + if (!CanSpawnMore()) { return; } + + int minAmount = Math.Min((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y), + maxAmount = Math.Max((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y); + + int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced); + + Vector2 offset = SpawnAreaOffset; + offset.Y = -offset.Y; + + switch (SpawnAreaShape) + { + case AreaShape.Circle: + { + var (x, y) = item.WorldPosition + offset; + + for (int i = 0; i < Math.Max(1, amount); i++) + { + float angle = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi); + float distance = Rand.Range(0, SpawnAreaRadius, Rand.RandSync.Unsynced); + Vector2 spawnPos = new Vector2(x + distance * (float)Math.Cos(angle), y + distance * (float)Math.Sin(angle)); + + SpawnEntity(spawnPos); + } + break; + } + case AreaShape.Rectangle: + { + RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw: false); + + for (int i = 0; i < Math.Max(1, amount); i++) + { + float minX = Math.Min(rect.Left, rect.Right), + maxX = Math.Max(rect.Left, rect.Right), + minY = Math.Min(rect.Top, rect.Bottom), + maxY = Math.Max(rect.Top, rect.Bottom); + + Vector2 spawnPos = new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced)); + + SpawnEntity(spawnPos); + } + break; + } + } + + void SpawnEntity(Vector2 pos) + { + if (!string.IsNullOrWhiteSpace(SpeciesName)) + { + string[] allSpecies = SpeciesName.Split(','); + string species = allSpecies.GetRandom().Trim(); + Entity.Spawner?.AddToSpawnQueue(species, pos); + } + else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) + { + string[] allItems = ItemIdentifier.Split(','); + string itemIdentifier = allItems.GetRandom().Trim(); + ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier); + if (prefab is null) { return; } + + if (item.Submarine is { } sub) + { + pos -= sub.Position; + } + + Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..ae34d86fe --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent, IServerSerializable + { + private readonly string materialName; + + private Character targetCharacter; + private AfflictionPrefab selectedEffect, selectedTaintedEffect; + + [Serialize("", true)] + public string Effect + { + get; + set; + } + + [Serialize("geneticmaterialdebuff", true)] + public string TaintedEffect + { + get; + set; + } + + private bool tainted; + [Serialize(false, true)] + public bool Tainted + { + get { return tainted; } + private set + { + if (!value) { return; } + tainted = true; + item.AllowDeconstruct = false; + if (!string.IsNullOrEmpty(TaintedEffect)) + { + selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a => + a.Identifier.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase) || + a.AfflictionType.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + } + } + } + + //only for saving the selected tainted effect + [Serialize("", true)] + public string SelectedTaintedEffect + { + get { return selectedTaintedEffect?.Identifier ?? string.Empty; } + private set + { + if (string.IsNullOrEmpty(value)) { return; } + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.Identifier == value); + } + } + + public GeneticMaterial(Item item, XElement element) + : base(item, element) + { + string nameId = element.GetAttributeString("nameidentifier", ""); + if (!string.IsNullOrEmpty(nameId)) + { + materialName = TextManager.Get(nameId); + } + if (!string.IsNullOrEmpty(Effect)) + { + selectedEffect = AfflictionPrefab.Prefabs.Where(a => + a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || + a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + } + } + + [Serialize(3.0f, false)] + public float ConditionIncreaseOnCombineMin { get; set; } + + [Serialize(8.0f, false)] + public float ConditionIncreaseOnCombineMax { get; set; } + + public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial) + { + return !tainted && otherGeneticMaterial != null && !otherGeneticMaterial.tainted && item.AllowDeconstruct && otherGeneticMaterial.item.AllowDeconstruct; + } + + public override void Equip(Character character) + { + if (character == null) { return; } + IsActive = true; + + if (targetCharacter != null) { return; } + + if (tainted) + { + if (selectedTaintedEffect != null) + { + float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; + character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); + var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (existingAffliction != null) + { + existingAffliction.Strength = selectedTaintedEffectStrength; + } + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + if (selectedEffect != null) + { + ApplyStatusEffects(ActionType.OnWearing, 1.0f); + float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; + character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); + var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (existingAffliction != null) + { + existingAffliction.Strength = selectedEffectStrength; + } + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif + } + foreach (Item containedItem in item.ContainedItems) + { + containedItem.GetComponent()?.Equip(character); + } + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (targetCharacter != null) + { + var rootContainer = item.GetRootContainer(); + if (!targetCharacter.HasEquippedItem(item) && + (rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface))) + { + item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter); + targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength); + if (tainted) + { + targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength); + } + targetCharacter = null; + IsActive = false; + } + } + } + + public bool Combine(GeneticMaterial otherGeneticMaterial, Character user) + { + if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } + + float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); + conditionIncrease += user.GetStatValue(StatTypes.GeneticMaterialRefineBonus); + if (item.Prefab == otherGeneticMaterial.item.Prefab) + { + item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease; + float taintedProbability = GetTaintedProbabilityOnRefine(user); + if (taintedProbability >= Rand.Range(0.0f, 1.0f)) + { + MakeTainted(); + } + return true; + } + else + { + item.Condition = otherGeneticMaterial.Item.Condition = + (item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f + conditionIncrease; + item.OwnInventory?.TryPutItem(otherGeneticMaterial.Item, user: null); + item.AllowDeconstruct = false; + otherGeneticMaterial.Item.AllowDeconstruct = false; + if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f)) + { + MakeTainted(); + } + return false; + } + } + + private float GetTaintedProbabilityOnRefine(Character user) + { + if (user == null) { return 1.0f; } + float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f); + probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user)); + return MathHelper.Clamp(probability, 0.0f, 1.0f); + } + + private float GetTaintedProbabilityOnCombine(Character user) + { + if (user == null) { return 1.0f; } + float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine); + return MathHelper.Clamp(probability, 0.0f, 1.0f); + } + + private void MakeTainted() + { + if (GameMain.NetworkMember?.IsClient ?? false) { return; } + Tainted = true; +#if SERVER + item.CreateServerEvent(this); +#endif + } + + public static string TryCreateName(ItemPrefab prefab, XElement element) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals(nameof(GeneticMaterial), StringComparison.OrdinalIgnoreCase)) + { + string nameId = subElement.GetAttributeString("nameidentifier", ""); + if (!string.IsNullOrEmpty(nameId)) + { + return prefab.Name.Replace("[type]", TextManager.Get(nameId, returnNull: true) ?? nameId); + } + } + } + return prefab.Name; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index cbfc92b1a..e88c44e43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; - private readonly PhysicsBody body; + private PhysicsBody body; public PhysicsBody Pusher { get; @@ -79,6 +79,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] + public bool UseHandRotationForHoldAngle + { + get; + set; + } + [Serialize(false, false, description: "Can the item be attached to walls.")] public bool Attachable { @@ -93,6 +100,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] + public bool LimitedAttachable + { + get; + set; + } + [Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] public bool AttachedByDefault { @@ -154,7 +168,8 @@ namespace Barotrauma.Items.Components BodyType = BodyType.Dynamic, CollidesWith = Physics.CollisionCharacter, CollisionCategories = Physics.CollisionItemBlocking, - Enabled = false + Enabled = false, + UserData = "Holdable.Pusher" }; Pusher.FarseerBody.OnCollision += OnPusherCollision; Pusher.FarseerBody.FixedRotation = false; @@ -205,7 +220,6 @@ namespace Barotrauma.Items.Components } } } - characterUsable = element.GetAttributeBool("characterusable", true); } @@ -247,6 +261,7 @@ namespace Barotrauma.Items.Components private void Drop(bool dropConnectedWires, Character dropper) { + GetRope()?.Snap(); if (dropConnectedWires) { DropConnectedWires(dropper); @@ -558,6 +573,15 @@ namespace Barotrauma.Items.Components PickKey = InputType.Select; } + public override void ParseMsg() + { + base.ParseMsg(); + if (Attachable) + { + prevMsg = DisplayMsg; + } + } + public override bool Use(float deltaTime, Character character = null) { if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } @@ -567,6 +591,28 @@ namespace Barotrauma.Items.Components if (!character.IsKeyDown(InputType.Aim)) { return false; } if (!CanBeAttached(character)) { return false; } + if (LimitedAttachable) + { + if (character?.Info == null) + { + DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!"); + return false; + } + Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true); + Structure attachTarget = Structure.GetAttachTarget(attachPos); + + int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int currentlyAttachedCount = Item.ItemList.Count( + i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); + if (currentlyAttachedCount >= maxAttachableCount) + { +#if CLIENT + GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); +#endif + return false; + } + } + if (GameMain.NetworkMember != null) { if (character != Character.Controlled) @@ -596,10 +642,13 @@ namespace Barotrauma.Items.Components item.Drop(character); item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false); } + AttachToWall(); } + return true; + } - AttachToWall(); - + public override bool SecondaryUse(float deltaTime, Character character = null) + { return true; } @@ -661,6 +710,20 @@ namespace Barotrauma.Items.Components Update(deltaTime, cam); } + public Rope GetRope() + { + var rangedWeapon = Item.GetComponent(); + if (rangedWeapon != null) + { + var lastProjectile = rangedWeapon.LastProjectile; + if (lastProjectile != null) + { + return lastProjectile.Item.GetComponent(); + } + } + return null; + } + public override void Update(float deltaTime, Camera cam) { if (attachTargetCell != null) @@ -715,9 +778,18 @@ namespace Barotrauma.Items.Components scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle); + if (!aim) + { + var rope = GetRope(); + if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null) + { + rope.Snap(); + } + } } else { + GetRope()?.Snap(); Limb equipLimb = null; if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head)) { @@ -780,7 +852,19 @@ namespace Barotrauma.Items.Components DeattachFromWall(); } } - + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + attachTargetCell = null; + if (Pusher != null) + { + Pusher.Remove(); + Pusher = null; + } + body = null; + } + public override XElement Save(XElement parentElement) { if (!attachable) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index d426b1a70..8fcf6d265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -1,11 +1,29 @@ -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class IdCard : Pickable { + [Serialize(CharacterTeamType.None, true, alwaysUseInstanceValues: true)] + public CharacterTeamType TeamID + { + get; + set; + } + + [Serialize(0, true, alwaysUseInstanceValues: true)] + public int SubmarineSpecificID + { + get; + set; + } + + private JobPrefab cachedJobPrefab; + private string cachedName; + public IdCard(Item item, XElement element) : base(item, element) { @@ -13,29 +31,33 @@ namespace Barotrauma.Items.Components public void Initialize(CharacterInfo info) { - if (info == null) return; + if (info == null) { return; } if (info.Job?.Prefab != null) { item.AddTag("jobid:" + info.Job.Prefab.Identifier); } + TeamID = info.TeamID; + var head = info.Head; - if (info != null && head != null) - { - item.AddTag("gender:" + head.gender.ToString().ToLowerInvariant()); - item.AddTag("race:" + head.race.ToString()); - item.AddTag("headspriteid:" + info.HeadSpriteId.ToString()); - item.AddTag("hairindex:" + head.HairIndex); - item.AddTag("beardindex:" + head.BeardIndex); - item.AddTag("moustacheindex:" + head.MoustacheIndex); - item.AddTag("faceattachmentindex:" + head.FaceAttachmentIndex); + if (head == null) { return; } + + if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); } + if (info.HasRaces) { item.AddTag($"race:{head.race}"); } + item.AddTag($"headspriteid:{info.HeadSpriteId}"); + item.AddTag($"hairindex:{head.HairIndex}"); + item.AddTag($"beardindex:{head.BeardIndex}"); + item.AddTag($"moustacheindex:{head.MoustacheIndex}"); + item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}"); + item.AddTag($"haircolor:{head.HairColor.ToStringHex()}"); + item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}"); + item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}"); - if (head.SheetIndex != null) - { - item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y); - } + if (head.SheetIndex != null) + { + item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}"); } } @@ -50,5 +72,48 @@ namespace Barotrauma.Items.Components base.Unequip(character); character.Info?.CheckDisguiseStatus(true, this); } + + public JobPrefab GetJob() + { + if (cachedJobPrefab != null) + { + return cachedJobPrefab; + } + + foreach (string tag in item.GetTags()) + { + if (tag.StartsWith("jobid:")) + { + string jobIdentifier = tag.Split(':').Last(); + if (JobPrefab.Get(jobIdentifier) is { } jobPrefab) + { + cachedJobPrefab = jobPrefab; + return jobPrefab; + } + } + } + + return null; + } + + public string GetName() + { + if (cachedName != null) + { + return cachedName; + } + + foreach (string tag in item.GetTags()) + { + if (tag.StartsWith("name:")) + { + string ownerName = tag.Split(':').Last(); + cachedName = ownerName; + return ownerName; + } + } + + return null; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index f59597c61..299afe98d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -50,6 +50,17 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(true, false)] + public bool Swing { get; set; } + + [Editable, Serialize("2.0, 0.0", false)] + public Vector2 SwingPos { get; set; } + + [Editable, Serialize("3.0, -1.0", false)] + public Vector2 SwingForce { get; set; } + + public bool Hitting { get { return hitting; } } + /// /// Defines items that boost the weapon functionality, like battery cell for stun batons. /// @@ -61,12 +72,13 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } - Attack = new Attack(subElement, item.Name + ", MeleeWeapon"); - Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()); + Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item) + { + DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()) + }; } item.IsShootable = true; - // TODO: should define this in xml if we have melee weapons that don't require aim to use - item.RequireAimToUse = true; + item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); } @@ -80,6 +92,9 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null || reloadTimer > 0.0f) { return false; } +#if CLIENT + if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; } +#endif if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; } //don't allow hitting if the character is already hitting with another weapon @@ -92,10 +107,10 @@ namespace Barotrauma.Items.Components SetUser(character); - if (hitPos < MathHelper.PiOver4) { return false; } + if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; } ActivateNearbySleepingCharacters(); - reloadTimer = reload; + reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed)); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; @@ -103,20 +118,28 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; item.body.PhysEnabled = true; - if (!character.AnimController.InWater) + if (Swing && !character.AnimController.InWater) { foreach (Limb l in character.AnimController.Limbs) { if (l.IsSevered) { continue; } - if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; } - if (l.type == LimbType.Head || l.type == LimbType.Torso) + Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass; + switch (l.type) { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f)); + case LimbType.Torso: + force *= 2; + break; + case LimbType.Legs: + case LimbType.LeftFoot: + case LimbType.LeftThigh: + case LimbType.LeftLeg: + case LimbType.RightFoot: + case LimbType.RightThigh: + case LimbType.RightLeg: + force = Vector2.Zero; + break; } - else - { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f)); - } + l.body.ApplyLinearImpulse(force); } } @@ -152,9 +175,16 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (!item.body.Enabled) { impactQueue.Clear(); return; } - if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; } - + if (!item.body.Enabled) + { + impactQueue.Clear(); + return; + } + if (picker == null && !picker.HeldItems.Contains(item)) + { + impactQueue.Clear(); + IsActive = false; + } while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); @@ -162,38 +192,48 @@ namespace Barotrauma.Items.Components } //in case handling the impact does something to the picker if (picker == null) { return; } - reloadTimer -= deltaTime; - if (reloadTimer < 0) { reloadTimer = 0; } - - if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; } - + if (reloadTimer < 0) + { + reloadTimer = 0; + } + if (!picker.IsKeyDown(InputType.Aim) && !hitting) + { + hitPos = 0.0f; + } ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - - if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } - + if (item.body.Dir != picker.AnimController.Dir) + { + item.FlipX(relativeToSub: false); + } AnimController ac = picker.AnimController; - - //TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items) if (!hitting) { - bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; if (aim) { - hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos); + hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); } else { hitPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { - hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f); - ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f) - if (hitPos < -MathHelper.PiOver2) + // TODO: We might want to make this configurable + hitPos -= deltaTime * 15f; + if (Swing) + { + ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle); + } + else + { + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + } + if (hitPos < -MathHelper.Pi) { RestoreCollision(); hitting = false; @@ -266,16 +306,10 @@ namespace Barotrauma.Items.Components return false; } - Character targetCharacter = null; - Limb targetLimb = null; - Structure targetStructure = null; - Item targetItem = null; - - if (f2.Body.UserData is Limb) + if (f2.Body.UserData is Limb targetLimb) { - targetLimb = (Limb)f2.Body.UserData; if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; } - targetCharacter = targetLimb.character; + var targetCharacter = targetLimb.character; if (targetCharacter == picker) { return false; } if (AllowHitMultiple) { @@ -287,9 +321,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if (f2.Body.UserData is Character) + else if (f2.Body.UserData is Character targetCharacter) { - targetCharacter = (Character)f2.Body.UserData; if (targetCharacter == picker || targetCharacter == User) { return false; } targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways if (AllowHitMultiple) @@ -302,9 +335,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if (f2.Body.UserData is Structure) + else if (f2.Body.UserData is Structure targetStructure) { - targetStructure = (Structure)f2.Body.UserData; if (AllowHitMultiple) { if (hitTargets.Contains(targetStructure)) { return true; } @@ -315,9 +347,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetStructure); } - else if (f2.Body.UserData is Item) + else if (f2.Body.UserData is Item targetItem) { - targetItem = (Item)f2.Body.UserData; if (AllowHitMultiple) { if (hitTargets.Contains(targetItem)) { return true; } @@ -350,12 +381,11 @@ namespace Barotrauma.Items.Components Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; - Structure targetStructure = target.UserData as Structure; - Item targetItem = target.UserData as Item; - if (Attack != null) { Attack.SetUser(User); + Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); + Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); if (targetLimb != null) { @@ -369,12 +399,12 @@ namespace Barotrauma.Items.Components targetCharacter.LastDamageSource = item; Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); } - else if (targetStructure != null) + else if (target.UserData is Structure targetStructure) { if (targetStructure.Removed) { return; } Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } - else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) + else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index dbac3d4fb..49abae0d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Abilities; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -73,12 +74,15 @@ namespace Barotrauma.Items.Components if (PickingTime > 0.0f) { + var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab); + picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); + if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue) { #if SERVER item.CreateServerEvent(this); #endif - pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, PickingTime)); + pickingCoroutine = CoroutineManager.StartCoroutine(WaitForPick(picker, abilityPickingTime.Value)); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 9e6ad682a..39e61fcb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -66,18 +66,18 @@ namespace Barotrauma.Items.Components foreach (Limb limb in character.AnimController.Limbs) { if (limb.WearingItems.Find(w => w.WearableComponent.Item == item) == null) { continue; } - limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + limb.body.ApplyForce(propulsion); } - character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + character.AnimController.Collider.ApplyForce(propulsion); if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) { - character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); } if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) { - character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 8a73e00a7..a07db8e19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,10 +1,11 @@ -using Barotrauma.Networking; +using Barotrauma.Abilities; +using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Collision; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Xml.Linq; @@ -52,17 +53,38 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + public float MaxChargeTime + { + get; + private set; + } + + private enum ChargingState + { + Inactive, + WindingUp, + WindingDown, + } + private ChargingState currentChargingState; + public Vector2 TransformedBarrelPos { get { Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation); Vector2 flippedPos = barrelPos; - if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X; - return Vector2.Transform(flippedPos, bodyTransform); + if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } + return Vector2.Transform(flippedPos, bodyTransform) * item.Scale; } } - + + + public Projectile LastProjectile { get; private set; } + + private float currentChargeTime; + private bool tryingToCharge; + public RangedWeapon(Item item, XElement element) : base(item, element) { @@ -88,10 +110,41 @@ namespace Barotrauma.Items.Components if (ReloadTimer < 0.0f) { ReloadTimer = 0.0f; - IsActive = false; + // was this an optimization or related to something else? it cannot occur for charge-type weapons + //IsActive = false; + if (MaxChargeTime == 0.0f) + { + IsActive = false; + return; + } } + + float previousChargeTime = currentChargeTime; + + float chargeDeltaTime = tryingToCharge && ReloadTimer <= 0f ? deltaTime : -deltaTime; + currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime); + + tryingToCharge = false; + + if (currentChargeTime == 0f) + { + currentChargingState = ChargingState.Inactive; + } + else if (currentChargeTime < previousChargeTime) + { + currentChargingState = ChargingState.WindingDown; + } + else + { + // if we are charging up or at maxed charge, remain winding up + currentChargingState = ChargingState.WindingUp; + } + + UpdateProjSpecific(deltaTime); } + partial void UpdateProjSpecific(float deltaTime); + private float GetSpread(Character user) { float degreeOfFailure = 1.0f - DegreeOfSuccess(user); @@ -102,11 +155,20 @@ namespace Barotrauma.Items.Components private readonly List limbBodies = new List(); public override bool Use(float deltaTime, Character character = null) { + tryingToCharge = true; if (character == null || character.Removed) { return false; } if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; } + if (currentChargeTime < MaxChargeTime) { return false; } IsActive = true; - ReloadTimer = reload; + ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + currentChargeTime = 0f; + + if (character != null) + { + var abilityItem = new AbilityItem(item); + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem); + } if (item.AiTarget != null) { @@ -136,7 +198,13 @@ namespace Barotrauma.Items.Components Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false); + var lastProjectile = LastProjectile; + if (lastProjectile != projectile) + { + lastProjectile?.Item.GetComponent()?.Snap(); + } + float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) { @@ -145,6 +213,7 @@ namespace Barotrauma.Items.Components projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); Item.RemoveContained(projectile.Item); } + LastProjectile = projectile; } LaunchProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index ddf06b33f..bc9c658ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -523,7 +523,20 @@ namespace Barotrauma.Items.Components ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure }); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); - targetStructure.AddDamage(sectionIndex, -StructureFixAmount * degreeOfSuccess, user); + + float structureFixAmount = StructureFixAmount; + if (structureFixAmount >= 0f) + { + structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureRepairMultiplier); + structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureRepairMultiplier); + } + else + { + structureFixAmount *= 1 + user.GetStatValue(StatTypes.RepairToolStructureDamageMultiplier); + structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier); + } + + targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user); //if the next section is small enough, apply the effect to it as well //(to make it easier to fix a small "left-over" section) @@ -535,7 +548,7 @@ namespace Barotrauma.Items.Components (nextSectionLength > 0 && nextSectionLength < Structure.WallSectionSize * 0.3f)) { //targetStructure.HighLightSection(sectionIndex + i); - targetStructure.AddDamage(sectionIndex + i, -StructureFixAmount * degreeOfSuccess); + targetStructure.AddDamage(sectionIndex + i, -structureFixAmount * degreeOfSuccess); } } return true; @@ -606,7 +619,8 @@ namespace Barotrauma.Items.Components levelResource.requiredItems.Any() && levelResource.HasRequiredItems(user, addMessage: false)) { - levelResource.DeattachTimer += deltaTime; + float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); + levelResource.DeattachTimer += addedDetachTime; #if CLIENT Character.Controlled?.UpdateHUDProgressBar( this, @@ -698,7 +712,7 @@ namespace Barotrauma.Items.Components humanAnim.Crouching = true; } } - if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater)) + if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater)) { // Steer closer if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 9dbf4dd16..66b0e88f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -123,18 +123,18 @@ namespace Barotrauma.Items.Components if (aim) { throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); } else { throwPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); if (throwPos < 0) { @@ -169,8 +169,8 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; midAir = true; - ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + ac.GetLimb(LimbType.Head)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + ac.GetLimb(LimbType.Torso)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index a5e19f9ac..ad9965f0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -725,7 +725,27 @@ namespace Barotrauma.Items.Components bool CheckItems(RelatedItem relatedItem, IEnumerable itemList) { - bool Predicate(Item it) => it != null && it.Condition > 0.0f && relatedItem.MatchesItem(it); + bool Predicate(Item it) + { + if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; } + if (item.Submarine != null) + { + var idCard = it.GetComponent(); + if (idCard != null) + { + //id cards don't work in enemy subs (except on items that only require the default "idcard" tag) + if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard")) + { + return false; + } + else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID) + { + return false; + } + } + } + return true; + }; bool shouldBreak = false; bool inEditor = false; #if CLIENT @@ -761,7 +781,7 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null) + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f) { if (statusEffectLists == null) { return; } @@ -773,7 +793,13 @@ namespace Barotrauma.Items.Components { if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } - item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, false, false, worldPosition); + item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); + if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) + { + effect.AfflictionMultiplier = applyOnUserFraction; + item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); + effect.AfflictionMultiplier = 1.0f; + } reducesCondition |= effect.ReducesItemCondition(); } //if any of the effects reduce the item's condition, set the user for OnBroken effects as well @@ -953,7 +979,7 @@ namespace Barotrauma.Items.Components } } - public void ParseMsg() + public virtual void ParseMsg() { string msg = TextManager.Get(Msg, true); if (msg != null) @@ -976,7 +1002,7 @@ namespace Barotrauma.Items.Components AIObjectiveContainItem containObjective = null; if (character.AIController is HumanAIController aiController) { - containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) + containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) { targetItemCount = itemCount, Equip = equip, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 5502221ae..27b8542ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics; +using System.Collections.Immutable; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -23,6 +25,28 @@ namespace Barotrauma.Items.Components } } + class SlotRestrictions + { + public readonly int MaxStackSize; + public readonly List ContainableItems; + + public SlotRestrictions(int maxStackSize, List containableItems) + { + MaxStackSize = maxStackSize; + ContainableItems = containableItems; + } + + public bool MatchesItem(Item item) + { + return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(item)); + } + + public bool MatchesItem(ItemPrefab itemPrefab) + { + return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab)); + } + } + private bool alwaysContainedItemsSpawned; public ItemInventory Inventory; @@ -37,7 +61,7 @@ namespace Barotrauma.Items.Components public int Capacity { get { return capacity; } - set { capacity = Math.Max(value, 1); } + set { capacity = Math.Max(value, 0); } } //how many items can be contained @@ -62,17 +86,12 @@ namespace Barotrauma.Items.Components } } -#if DEBUG - [Editable] -#endif [Serialize("0.0,0.0", false, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] public Vector2 ItemPos { get; set; } -#if DEBUG - [Editable] -#endif [Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] public Vector2 ItemInterval { get; set; } + [Serialize(100, false, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } @@ -90,6 +109,12 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false)] + public bool AllowSwappingContainedItems + { + get; + set; + } [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained @@ -98,6 +123,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false)] + public bool AllowAccess { get; set; } + [Serialize(false, false)] public bool AccessOnlyWhenBroken { get; set; } @@ -140,13 +168,29 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Should the items be injected into the user.")] + public bool AutoInject + { + get; + set; + } + + [Serialize(0.5f, false, description: "The health threshold that the user must reach in order to activate the autoinjection.")] + public float AutoInjectThreshold + { + get; + set; + } + [Serialize(false, false)] public bool RemoveContainedItemsOnDeconstruct { get; set; } + private SlotRestrictions[] slotRestrictions; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); - if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; } + if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; } if (!isRestrictionsDefined) { return true; } return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id)); } @@ -154,22 +198,22 @@ namespace Barotrauma.Items.Components public bool ShouldBeContained(Item item, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); - if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; } + if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; } if (!isRestrictionsDefined) { return true; } return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); } - public List ContainableItems { get; private set; } = new List(); - - public IEnumerable GetContainableItemIdentifiers => ContainableItems.SelectMany(ri => ri.Identifiers); + private ImmutableHashSet containableItemIdentifiers; + public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; public override bool RecreateGUIOnResolutionChange => true; public ItemContainer(Item item, XElement element) - : base (item, element) + : base(item, element) { - Inventory = new ItemInventory(item, this, capacity, SlotsPerRow); - + int totalCapacity = capacity; + + List containableItems = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -181,34 +225,95 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); continue; } - ContainableItems.Add(containable); + containableItems ??= new List(); + containableItems.Add(containable); + break; + case "subcontainer": + totalCapacity += subElement.GetAttributeInt("capacity", 1); break; } } + Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); + slotRestrictions = new SlotRestrictions[totalCapacity]; + for (int i = 0; i < capacity; i++) + { + slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems); + } + int subContainerIndex = capacity; + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } + + int subCapacity = subElement.GetAttributeInt("capacity", 1); + int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); + + List subContainableItems = null; + foreach (XElement subSubElement in subElement.Elements()) + { + if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } + + RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); + if (containable == null) + { + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + continue; + } + subContainableItems ??= new List(); + subContainableItems.Add(containable); + } + + for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) + { + slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems); + } + subContainerIndex += subCapacity; + } + capacity = totalCapacity; InitProjSpecific(element); } + public int GetMaxStackSize(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= capacity) + { + return 0; + } + return slotRestrictions[slotIndex].MaxStackSize; + } + partial void InitProjSpecific(XElement element); public void OnItemContained(Item containedItem) { item.SetContainedItemPositions(); - - RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem)); - if (ri != null) + + int index = Inventory.FindIndex(containedItem); + if (index >= 0 && index < slotRestrictions.Length) { - activeContainedItems.RemoveAll(i => i.Item == containedItem); - foreach (StatusEffect effect in ri.statusEffects) + if (slotRestrictions[index].ContainableItems != null) { - activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken)); + activeContainedItems.RemoveAll(i => i.Item == containedItem); + foreach (var containableItem in slotRestrictions[index].ContainableItems) + { + if (!containableItem.MatchesItem(containedItem)) { continue; } + foreach (StatusEffect effect in containableItem.statusEffects) + { + activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken)); + } + } } - } + } //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } + public override void Move(Vector2 amount) + { + SetContainedItemPositions(); + } + public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); @@ -219,13 +324,24 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { - if (ContainableItems.Count == 0) { return true; } - return ContainableItems.Find(c => c.MatchesItem(item)) != null; + return slotRestrictions.Any(s => s.MatchesItem(item)); } + + public bool CanBeContained(Item item, int index) + { + if (index < 0 || index >= capacity) { return false; } + return slotRestrictions[index].MatchesItem(item); + } + public bool CanBeContained(ItemPrefab itemPrefab) { - if (ContainableItems.Count == 0) { return true; } - return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null; + return slotRestrictions.Any(s => s.MatchesItem(itemPrefab)); + } + + public bool CanBeContained(ItemPrefab itemPrefab, int index) + { + if (index < 0 || index >= capacity) { return false; } + return slotRestrictions[index].MatchesItem(itemPrefab); } readonly List targets = new List(); @@ -237,9 +353,24 @@ namespace Barotrauma.Items.Components SpawnAlwaysContainedItems(); } - if (item.ParentInventory is CharacterInventory) + if (item.ParentInventory is CharacterInventory ownerInventory) { item.SetContainedItemPositions(); + + if (AutoInject) + { + if (ownerInventory?.Owner is Character ownerCharacter && + ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold && + ownerCharacter.HasEquippedItem(item)) + { + foreach (Item item in Inventory.AllItemsMod) + { + item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); + item.GetComponent()?.Equip(ownerCharacter); + } + } + } + } else if (item.body != null && item.body.Enabled && @@ -256,6 +387,7 @@ namespace Barotrauma.Items.Components foreach (var activeContainedItem in activeContainedItems) { Item contained = activeContainedItem.Item; + if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; @@ -275,11 +407,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { - return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { + if (!AllowAccess) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -299,11 +432,15 @@ namespace Barotrauma.Items.Components } } } + var abilityItem = new AbilityItem(item); + character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem); + return base.Select(character); } public override bool Pick(Character picker) { + if (!AllowAccess) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) @@ -331,7 +468,7 @@ namespace Barotrauma.Items.Components public override bool Combine(Item item, Character user) { if (!AllowDragAndDrop && user != null) { return false; } - if (!ContainableItems.Any(it => it.MatchesItem(item))) { return false; } + if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; } if (user != null && !user.CanAccessInventory(Inventory)) { return false; } if (Inventory.TryPutItem(item, user)) @@ -361,52 +498,65 @@ namespace Barotrauma.Items.Components Vector2 transformedItemInterval = ItemInterval * item.Scale; Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f); Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y); - if (item.body == null) + + if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero) { - if (item.FlippedX) - { - transformedItemPos.X = -transformedItemPos.X; - transformedItemPos.X += item.Rect.Width; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; - } - if (item.FlippedY) - { - transformedItemPos.Y = -transformedItemPos.Y; - transformedItemPos.Y -= item.Rect.Height; - transformedItemInterval.Y = -transformedItemInterval.Y; - transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; - } - transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.Rotation) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); - transformedItemPos = Vector2.Transform(transformedItemPos, transform); - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); - } + transformedItemPos = item.Position; } else { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - if (item.body.Dir == -1.0f) + if (item.body == null) { - transformedItemPos.X = -transformedItemPos.X; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + if (item.FlippedX) + { + transformedItemPos.X = -transformedItemPos.X; + transformedItemPos.X += item.Rect.Width; + transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + } + if (item.FlippedY) + { + transformedItemPos.Y = -transformedItemPos.Y; + transformedItemPos.Y -= item.Rect.Height; + transformedItemInterval.Y = -transformedItemInterval.Y; + transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; + } + transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.Rotation) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position; + transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); + transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); + } } - transformedItemPos = Vector2.Transform(transformedItemPos, transform); - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemPos += item.Position; - } + else + { + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + if (item.body.Dir == -1.0f) + { + transformedItemPos.X = -transformedItemPos.X; + transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + } + transformedItemPos = Vector2.Transform(transformedItemPos, transform); + transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); + transformedItemPos += item.Position; + } + } float currentRotation = itemRotation; if (item.body != null) { + currentRotation *= item.body.Dir; currentRotation += item.body.Rotation; } + else + { + currentRotation += MathHelper.ToRadians(-item.Rotation); + } int i = 0; Vector2 currentItemPos = transformedItemPos; @@ -461,6 +611,8 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems; + containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); if (item.Submarine == null || !item.Submarine.Loading) { SpawnAlwaysContainedItems(); @@ -488,7 +640,21 @@ namespace Barotrauma.Items.Components } itemIds = null; } - SpawnAlwaysContainedItems(); + + //outpost and ruins are loaded in multiple stages (each module is loaded separately) + //spawning items at this point during the generation will cause ID overlaps with the entities in the modules loaded afterwards + //so let's not spawn them at this point, but in the 1st Update() + if (item.Submarine?.Info != null && (item.Submarine.Info.IsOutpost || item.Submarine.Info.IsRuin)) + { + if (SpawnWithId.Length > 0) + { + IsActive = true; + } + } + else + { + SpawnAlwaysContainedItems(); + } } private void SpawnAlwaysContainedItems() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 575401584..e3eb12bcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Collections.Generic; @@ -14,6 +15,12 @@ namespace Barotrauma.Items.Components private bool hasPower; + private Character user; + + private float userDeconstructorSpeedMultiplier = 1.0f; + + private const float TinkeringSpeedIncrease = 1.5f; + private ItemContainer inputContainer, outputContainer; public ItemContainer InputContainer @@ -25,7 +32,10 @@ namespace Barotrauma.Items.Components { get { return outputContainer; } } - + + [Serialize(false, true)] + public bool DeconstructItemsSimultaneously { get; set; } + [Editable, Serialize(1.0f, true)] public float DeconstructionSpeed { get; set; } @@ -81,65 +91,177 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0.0f) { Voltage = 1.0f; } progressTimer += deltaTime * Math.Min(Voltage, 1.0f); - var targetItem = inputContainer.Inventory.LastOrDefault(); - if (targetItem == null) { return; } - - float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f; - - progressState = Math.Min(progressTimer / deconstructTime, 1.0f); - if (progressTimer > deconstructTime) + float tinkeringStrength = 0f; + if (repairable.IsTinkering) { - // In multiplayer, the server handles the deconstruction into new items - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + tinkeringStrength = repairable.TinkeringStrength; + } + // doesn't quite work properly, remaining time changes if tinkering stops + float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); - if (targetItem.Prefab.RandomDeconstructionOutput) + if (DeconstructItemsSimultaneously) + { + float deconstructTime = 0.0f; + foreach (Item targetItem in inputContainer.Inventory.AllItems) { - int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; - List deconstructItemIndexes = new List(); - for (int i = 0; i < targetItem.Prefab.DeconstructItems.Count; i++) - { - deconstructItemIndexes.Add(i); - } - List commonness = targetItem.Prefab.DeconstructItems.Select(i => i.Commonness).ToList(); - List products = new List(); - - for (int i = 0; i < amount; i++) - { - if (deconstructItemIndexes.Count < 1) { break; } - var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced); - products.Add(targetItem.Prefab.DeconstructItems[itemIndex]); - var removeIndex = deconstructItemIndexes.IndexOf(itemIndex); - deconstructItemIndexes.RemoveAt(removeIndex); - commonness.RemoveAt(removeIndex); - } - foreach (DeconstructItem deconstructProduct in products) - { - CreateDeconstructProduct(deconstructProduct); - } + deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier); } - else + + progressState = Math.Min(progressTimer / deconstructTime, 1.0f); + if (progressTimer > deconstructTime) { - foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems) + List items = inputContainer.Inventory.AllItems.ToList(); + foreach (Item targetItem in items) { - CreateDeconstructProduct(deconstructProduct); + if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; } + var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => + (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) && + (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))))); + + ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); + } +#if SERVER + item.CreateServerEvent(this); +#endif + progressTimer = 0.0f; + progressState = 0.0f; + + } + } + else + { + var targetItem = inputContainer.Inventory.LastOrDefault(); + if (targetItem == null) { return; } + + var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => + it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))); + + float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + + progressState = Math.Min(progressTimer / deconstructTime, 1.0f); + if (progressTimer > deconstructTime) + { + ProcessItem(targetItem, inputContainer.Inventory.AllItemsMod, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); + +#if SERVER + item.CreateServerEvent(this); +#endif + progressTimer = 0.0f; + progressState = 0.0f; + + } + } + } + + private void ProcessItem(Item targetItem, IEnumerable inputItems, List validDeconstructItems, bool allowRemove = true) + { + // In multiplayer, the server handles the deconstruction into new items + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + if (user != null && !user.Removed) + { + var abilityTargetItem = new AbilityItem(targetItem); + user.CheckTalents(AbilityEffectType.OnItemDeconstructed, abilityTargetItem); + } + + if (targetItem.Prefab.RandomDeconstructionOutput) + { + int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; + List deconstructItemIndexes = new List(); + for (int i = 0; i < validDeconstructItems.Count; i++) + { + deconstructItemIndexes.Add(i); + } + List commonness = validDeconstructItems.Select(i => i.Commonness).ToList(); + List products = new List(); + + for (int i = 0; i < amount; i++) + { + if (deconstructItemIndexes.Count < 1) { break; } + var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced); + products.Add(validDeconstructItems[itemIndex]); + var removeIndex = deconstructItemIndexes.IndexOf(itemIndex); + deconstructItemIndexes.RemoveAt(removeIndex); + commonness.RemoveAt(removeIndex); + } + + foreach (DeconstructItem deconstructProduct in products) + { + CreateDeconstructProduct(deconstructProduct, inputItems); + } + } + else + { + foreach (DeconstructItem deconstructProduct in validDeconstructItems) + { + CreateDeconstructProduct(deconstructProduct, inputItems); + } + } + + void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems) + { + float percentageHealth = targetItem.Condition / targetItem.MaxCondition; + + if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } + + if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); + return; + } + + float condition = deconstructProduct.CopyCondition ? + percentageHealth * itemPrefab.Health : + itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax); + + if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0) + { + foreach (Item otherItem in inputItems) + { + if (targetItem == otherItem) { continue; } + if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) + { + user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); + } + + var geneticMaterial1 = targetItem.GetComponent(); + var geneticMaterial2 = otherItem.GetComponent(); + if (geneticMaterial1 != null && geneticMaterial2 != null) + { + if (geneticMaterial1.Combine(geneticMaterial2, user)) + { + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddToRemoveQueue(otherItem); + } + allowRemove = false; + return; + } + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddToRemoveQueue(otherItem); + } } } - void CreateDeconstructProduct(DeconstructItem deconstructProduct) + int amount = 1; + + if (user != null && !user.Removed) { - float percentageHealth = targetItem.Condition / targetItem.Prefab.Health; - if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } + var itemsCreated = new AbilityValueItem(amount, targetItem.Prefab); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemsCreated); + amount = (int)itemsCreated.Value; - if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) - { - DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); - return; - } - - float condition = deconstructProduct.CopyCondition ? - percentageHealth * itemPrefab.Health : - itemPrefab.Health * deconstructProduct.OutCondition; + // used to spawn items directly into the deconstructor + var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer); + } + for (int i = 0; i < amount; i++) + { Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { for (int i = 0; i < outputContainer.Capacity; i++) @@ -153,36 +275,31 @@ namespace Barotrauma.Items.Components PutItemsToLinkedContainer(); }); } + } - if (targetItem.Prefab.AllowDeconstruct) + if (targetItem.AllowDeconstruct && allowRemove) + { + //drop all items that are inside the deconstructed item + foreach (ItemContainer ic in targetItem.GetComponents()) { - //drop all items that are inside the deconstructed item - foreach (ItemContainer ic in targetItem.GetComponents()) - { - if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } - ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); - } - inputContainer.Inventory.RemoveItem(targetItem); - Entity.Spawner.AddToRemoveQueue(targetItem); - MoveInputQueue(); - PutItemsToLinkedContainer(); + if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } + ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); + } + inputContainer.Inventory.RemoveItem(targetItem); + Entity.Spawner.AddToRemoveQueue(targetItem); + MoveInputQueue(); + PutItemsToLinkedContainer(); + } + else + { + if (!outputContainer.Inventory.CanBePut(targetItem) || (Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false)) + { + targetItem.Drop(dropper: null); } else { - if (!outputContainer.Inventory.CanBePut(targetItem)) - { - targetItem.Drop(dropper: null); - } - else - { - outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); - } + outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); } -#if SERVER - item.CreateServerEvent(this); -#endif - progressTimer = 0.0f; - progressState = 0.0f; } } @@ -190,7 +307,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (outputContainer.Inventory.IsEmpty()) { return; } - + foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) @@ -201,7 +318,7 @@ namespace Barotrauma.Items.Components if (itemContainer == null) { continue; } outputContainer.Inventory.AllItemsMod.ForEach(containedItem => itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true)); } - } + } } /// @@ -221,14 +338,54 @@ namespace Barotrauma.Items.Components } } + private IEnumerable<(Item item, DeconstructItem output)> GetAvailableOutputs(bool checkRequiredOtherItems = true) + { + var items = inputContainer.Inventory.AllItems; + foreach (Item inputItem in items) + { + if (!inputItem.AllowDeconstruct) { continue; } + foreach (var deconstructItem in inputItem.Prefab.DeconstructItems) + { + if (deconstructItem.RequiredDeconstructor.Length > 0) + { + if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + } + if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems) + { + if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))) { continue; } + bool validOtherItemFound = false; + foreach (Item otherInputItem in items) + { + if (otherInputItem == inputItem) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + + var geneticMaterial1 = inputItem.GetComponent(); + var geneticMaterial2 = otherInputItem.GetComponent(); + if (geneticMaterial1 != null && geneticMaterial2 != null) + { + if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; } + } + validOtherItemFound = true; + } + if (!validOtherItemFound) { continue; } + } + yield return (inputItem, deconstructItem); + } + } + } + private void SetActive(bool active, Character user = null) { PutItemsToLinkedContainer(); + this.user = user; + if (inputContainer.Inventory.IsEmpty()) { active = false; } IsActive = active; currPowerConsumption = IsActive ? powerConsumption : 0.0f; + userDeconstructorSpeedMultiplier = user != null ? 1f + user.GetStatValue(StatTypes.DeconstructorSpeedMultiplier) : 1f; + #if SERVER if (user != null) { @@ -241,10 +398,6 @@ namespace Barotrauma.Items.Components progressState = 0.0f; } -#if CLIENT - activateButton.Text = TextManager.Get(IsActive ? "DeconstructorCancel" : "DeconstructorDeconstruct"); -#endif - inputContainer.Inventory.Locked = IsActive; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 0e9457394..266c28fff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -73,6 +73,8 @@ namespace Barotrauma.Items.Components } } + private const float TinkeringForceIncrease = 1.5f; + public Engine(Item item, XElement element) : base(item, element) { @@ -113,31 +115,35 @@ namespace Barotrauma.Items.Components Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f); if (Math.Abs(Force) > 1.0f) { + float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); + float currForce = force * voltageFactor; + float condition = item.Condition / item.MaxCondition; + // Broken engine makes more noise. + float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); + UpdateAITargets(noise); //arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines float forceMultiplier = 0.1f; if (User != null) { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } + currForce *= maxForce * forceMultiplier; + if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + { + currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; + } - float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); - Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f); //less effective when in a bad condition - currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition); + currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } - item.Submarine.ApplyForce(currForce); + Vector2 forceVector = new Vector2(currForce, 0); + item.Submarine.ApplyForce(forceVector); UpdatePropellerDamage(deltaTime); - float maxChangeSpeed = 0.5f; - float modifier = 2; - float noise = MathUtils.NearlyEqual(0.0f, maxForce) ? 0.0f : currForce.Length() * forceMultiplier * modifier / maxForce; - float min = Math.Max(1 - maxChangeSpeed, 0); - float max = 1 + maxChangeSpeed; - UpdateAITargets(Math.Clamp(noise, min, max), deltaTime); #if CLIENT particleTimer -= deltaTime; if (particleTimer <= 0.0f) { - Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f; + Vector2 particleVel = -forceVector.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale, particleVel * Rand.Range(0.9f, 1.1f), 0.0f, item.CurrentHull); @@ -147,14 +153,14 @@ namespace Barotrauma.Items.Components } } - private void UpdateAITargets(float increaseSpeed, float deltaTime) + private void UpdateAITargets(float noise) { if (item.AiTarget != null) { - item.AiTarget.IncreaseSoundRange(deltaTime, increaseSpeed); + item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, noise / 100); if (item.CurrentHull != null && item.CurrentHull.AiTarget != null) { - // It's possible that some othe item increases the hull's soundrange more than the engine. + // It's possible that some other item increases the hull's soundrange more than the engine. item.CurrentHull.AiTarget.SoundRange = Math.Max(item.CurrentHull.AiTarget.SoundRange, item.AiTarget.SoundRange); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ba0fd4715..461ae0617 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Security.Cryptography; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -32,6 +32,8 @@ namespace Barotrauma.Items.Components [Serialize(1.0f, true)] public float SkillRequirementMultiplier { get; set; } + private const float TinkeringSpeedIncrease = 1.5f; + private enum FabricatorState { Active = 1, @@ -240,7 +242,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (fabricatedItem == null || !CanBeFabricated(fabricatedItem)) + var availableIngredients = GetAvailableIngredients(); + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) { CancelFabricating(); return; @@ -278,48 +281,91 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0) { Voltage = 1.0f; } - timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f); + float tinkeringStrength = 0f; + if (repairable.IsTinkering) + { + tinkeringStrength = repairable.TinkeringStrength; + } + float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; + + timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); + UpdateRequiredTimeProjSpecific(); if (timeUntilReady > 0.0f) { return; } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - var availableIngredients = GetAvailableIngredients(); - foreach (FabricationRecipe.RequiredItem ingredient in fabricatedItem.RequiredItems) - { - for (int i = 0; i < ingredient.Amount; i++) + fabricatedItem.RequiredItems.ForEach(requiredItem => { + for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { - var availableItem = availableIngredients.FirstOrDefault(it => - it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && - it.ConditionPercentage >= ingredient.MinCondition * 100.0f && - it.ConditionPercentage <= ingredient.MaxCondition * 100.0f); - if (availableItem == null) { continue; } - - if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition; - continue; + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => + { + return potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && + potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + }); + + if (availablePrefab == null) { continue; } + + if (requiredItem.UseCondition && availablePrefab.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 + { + availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition; + continue; + } + + availablePrefabs.Remove(availablePrefab); + Entity.Spawner.AddToRemoveQueue(availablePrefab); + inputContainer.Inventory.RemoveItem(availablePrefab); } - availableIngredients.Remove(availableItem); - Entity.Spawner.AddToRemoveQueue(availableItem); - inputContainer.Inventory.RemoveItem(availableItem); } + }); + + int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); + + var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem); + + int quality = 0; + if (user?.Info != null) + { + foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) + { + character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); + } + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); + + quality = GetFabricatedItemQuality(fabricatedItem, user); } - Character tempUser = user; - int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); - for (int i = 0; i < fabricatedItem.Amount; i++) + var tempUser = user; + for (int i = 0; i < (int)fabricationValueItem.Value; i++) { + float outCondition = fabricatedItem.OutCondition; if (i < amountFittingContainer) { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, + onSpawned: (Item spawnedItem) => + { + onItemSpawned(spawnedItem, tempUser); + spawnedItem.Quality = quality; + //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers + spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; + }); } else { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, + onSpawned: (Item spawnedItem) => + { + onItemSpawned(spawnedItem, tempUser); + spawnedItem.Quality = quality; + //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers + spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; + }); } } @@ -333,16 +379,19 @@ namespace Barotrauma.Items.Components } } } - if (user?.Info != null && !user.Removed) { foreach (Skill skill in fabricatedItem.RequiredSkills) { float userSkill = user.GetSkillLevel(skill.Identifier); + float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); + var addedSkillValue = new AbilityValueString(0f, skill.Identifier); + user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); + addedSkill += addedSkillValue.Value; + user.Info.IncreaseSkillLevel( skill.Identifier, - skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f), - user.Position + Vector2.UnitY * 150.0f); + addedSkill); } } @@ -363,26 +412,57 @@ namespace Barotrauma.Items.Components } } - partial void UpdateRequiredTimeProjSpecific(); - - private bool CanBeFabricated(FabricationRecipe fabricableItem) + private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { - if (fabricableItem == null) { return false; } - List availableIngredients = GetAvailableIngredients(); - return CanBeFabricated(fabricableItem, availableIngredients); + if (user == null) { return 0; } + if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } + int quality = 0; + float floatQuality = 0.0f; + foreach (string tag in fabricatedItem.TargetItem.Tags) + { + floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); + } + quality = (int)floatQuality; + + const int MaxCraftingSkill = 100; + + quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; + quality += FabricationDegreeOfSuccess(user, fabricatedItem.RequiredSkills) >= 0.5f ? 1 : 0; + return quality; } - private bool CanBeFabricated(FabricationRecipe fabricableItem, IEnumerable availableIngredients) + partial void UpdateRequiredTimeProjSpecific(); + + private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) { - if (fabricableItem == null) { return false; } - foreach (FabricationRecipe.RequiredItem requiredItem in fabricableItem.RequiredItems) + if (fabricableItem == null) { return false; } + if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } + + return fabricableItem.RequiredItems.All(requiredItem => { - if (availableIngredients.Count(it => IsItemValidIngredient(it, requiredItem)) < requiredItem.Amount) + int availablePrefabsAmount = 0; + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) { - return false; + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } + + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + foreach (Item availablePrefab in availablePrefabs) + { + if (availablePrefab.Condition / availablePrefab.Prefab.Health >= requiredItem.MinCondition && + availablePrefab.Condition / availablePrefab.Prefab.Health <= requiredItem.MaxCondition) + { + availablePrefabsAmount++; + } + + if (availablePrefabsAmount >= requiredItem.Amount) + { + return true; + } + } } - } - return true; + + return false; + }); } private float GetRequiredTime(FabricationRecipe fabricableItem, Character user) @@ -416,7 +496,7 @@ namespace Barotrauma.Items.Components /// Get a list of all items available in the input container and linked containers /// /// - private List GetAvailableIngredients() + private Dictionary> GetAvailableIngredients() { List availableIngredients = new List(); availableIngredients.AddRange(inputContainer.Inventory.AllItems); @@ -448,7 +528,19 @@ namespace Barotrauma.Items.Components } #endif - return availableIngredients; + Dictionary> ingredientsDictionary = new Dictionary>(); + for (int i = 0; i < availableIngredients.Count; i++) + { + var itemIdentifier = availableIngredients[i].prefab.Identifier; + if (!ingredientsDictionary.ContainsKey(itemIdentifier)) + { + ingredientsDictionary[itemIdentifier] = new List(availableIngredients.Count); + } + + ingredientsDictionary[itemIdentifier].Add(availableIngredients[i]); + } + + return ingredientsDictionary; } /// @@ -463,40 +555,41 @@ namespace Barotrauma.Items.Components bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; var availableIngredients = GetAvailableIngredients(); - foreach (var requiredItem in targetItem.RequiredItems) - { + targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { - var matchingItem = availableIngredients.Find(it => !usedItems.Contains(it) && IsItemValidIngredient(it, requiredItem)); - if (matchingItem == null) { continue; } + foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs) + { + if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } - availableIngredients.Remove(matchingItem); - - if (matchingItem.ParentInventory == inputContainer.Inventory) - { - //already in input container, all good - usedItems.Add(matchingItem); - } - else //in another inventory, we need to move the item - { - if (!inputContainer.Inventory.CanBePut(matchingItem)) + var availablePrefabs = availableIngredients[requiredPrefab.Identifier]; + var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab => { - var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); - unneededItem?.Drop(null, createNetworkEvent: !isClient); - } - inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient); - } - } - } - } + return !usedItems.Contains(potentialPrefab) && + potentialPrefab.ConditionPercentage >= requiredItem.MinCondition * 100.0f && + potentialPrefab.ConditionPercentage <= requiredItem.MaxCondition * 100.0f; + }); + if (availablePrefab == null) { continue; } - private bool IsItemValidIngredient(Item item, FabricationRecipe.RequiredItem requiredItem) - { - return - item != null && - requiredItem.ItemPrefabs.Contains(item.prefab) && - item.Condition / item.Prefab.Health >= requiredItem.MinCondition && - item.Condition / item.Prefab.Health <= requiredItem.MaxCondition; + availablePrefabs.Remove(availablePrefab); + + if (availablePrefab.ParentInventory == inputContainer.Inventory) + { + //already in input container, all good + usedItems.Add(availablePrefab); + } + else //in another inventory, we need to move the item + { + if (!inputContainer.Inventory.CanBePut(availablePrefab)) + { + var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); + unneededItem?.Drop(null, createNetworkEvent: !isClient); + } + inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient); + } + } + } + }); } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 8ffa96100..39049b9bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -7,10 +7,15 @@ namespace Barotrauma.Items.Components { partial class MiniMap : Powered { - class HullData + internal class HullData { - public float? Oxygen; - public float? Water; + public float? HullOxygenAmount, + HullWaterAmount; + + public float? ReceivedOxygenAmount, + ReceivedWaterAmount; + + public readonly HashSet Cards = new HashSet(); public bool Distort; public float DistortionTimer; @@ -45,17 +50,38 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(true, true, description: "Enable hull status mode.")] + public bool EnableHullStatus + { + get; + set; + } + + [Editable, Serialize(true, true, description: "Enable electrical view mode.")] + public bool EnableElectricalView + { + get; + set; + } + + [Editable, Serialize(true, true, description: "Enable item finder mode.")] + public bool EnableItemFinder + { + get; + set; + } + public MiniMap(Item item, XElement element) : base(item, element) { IsActive = true; hullDatas = new Dictionary(); - InitProjSpecific(element); + InitProjSpecific(); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(); - public override void Update(float deltaTime, Camera cam) + public override void Update(float deltaTime, Camera cam) { //periodically reset all hull data //(so that outdated hull info won't be shown if detectors stop sending signals) @@ -65,13 +91,29 @@ namespace Barotrauma.Items.Components { if (!hullData.Distort) { - hullData.Oxygen = null; - hullData.Water = null; + hullData.ReceivedOxygenAmount = null; + hullData.ReceivedWaterAmount = null; } } resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); } +#if CLIENT + if (cardRefreshTimer > cardRefreshDelay) + { + if (item.Submarine is { } sub) + { + UpdateIDCards(sub); + } + + cardRefreshTimer = 0; + } + else + { + cardRefreshTimer += deltaTime; + } +#endif + currPowerConsumption = powerConsumption; currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); @@ -81,7 +123,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } } - + public override bool Pick(Character picker) { return picker != null; @@ -99,30 +141,49 @@ namespace Barotrauma.Items.Components hullDatas.Add(sourceHull, hullData); } - if (hullData.Distort) return; + if (hullData.Distort) { return; } switch (connection.Name) { case "water_data_in": //cheating a bit because water detectors don't actually send the water level + float waterAmount; if (source.GetComponent() == null) { - hullData.Water = Rand.Range(0.0f, 1.0f); + waterAmount = Rand.Range(0.0f, 1.0f); } else { - hullData.Water = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); + waterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f); + } + hullData.ReceivedWaterAmount = waterAmount; + foreach (var linked in sourceHull.linkedTo) + { + if (!(linked is Hull linkedHull)) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedWaterAmount = waterAmount; } break; case "oxygen_data_in": - float oxy; - - if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy)) + if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy)) { oxy = Rand.Range(0.0f, 100.0f); } - - hullData.Oxygen = oxy; + hullData.ReceivedOxygenAmount = oxy; + foreach (var linked in sourceHull.linkedTo) + { + if (!(linked is Hull linkedHull)) { continue; } + if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) + { + linkedHullData = new HullData(); + hullDatas.Add(linkedHull, linkedHullData); + } + linkedHullData.ReceivedOxygenAmount = oxy; + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 8e836bcf1..d76ec970c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -11,9 +11,12 @@ namespace Barotrauma.Items.Components private float generatedAmount; //key = vent, float = total volume of the hull the vent is in and the hulls connected to it - private Dictionary ventList; + private List<(Vent vent, float hullVolume)> ventList; private float totalHullVolume; + + private float ventUpdateTimer; + const float VentUpdateInterval = 5.0f; public float CurrFlow { @@ -31,6 +34,8 @@ namespace Barotrauma.Items.Components public OxygenGenerator(Item item, XElement element) : base(item, element) { + //randomize update timer so all oxygen generators don't update at the same time + ventUpdateTimer = Rand.Range(0.0f, VentUpdateInterval); IsActive = true; } @@ -64,7 +69,7 @@ namespace Barotrauma.Items.Components //20% condition = 4% CurrFlow *= conditionMult * conditionMult; - UpdateVents(CurrFlow); + UpdateVents(CurrFlow, deltaTime); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -75,7 +80,9 @@ namespace Barotrauma.Items.Components private void GetVents() { - ventList = new Dictionary(); + totalHullVolume = 0.0f; + ventList ??= new List<(Vent vent, float hullVolume)>(); + ventList.Clear(); foreach (MapEntity entity in item.linkedTo) { if (!(entity is Item linkedItem)) { continue; } @@ -83,30 +90,40 @@ namespace Barotrauma.Items.Components Vent vent = linkedItem.GetComponent(); if (vent?.Item.CurrentHull == null) { continue; } - ventList.Add(vent, 0.0f); - foreach (Hull connectedHull in vent.Item.CurrentHull.GetConnectedHulls(includingThis: true, searchDepth: 10, ignoreClosedGaps: true)) - { + totalHullVolume += vent.Item.CurrentHull.Volume; + ventList.Add((vent, vent.Item.CurrentHull.Volume)); + } + + for (int i = 0; i < ventList.Count; i++) + { + Vent vent = ventList[i].vent; + foreach (Hull connectedHull in vent.Item.CurrentHull.GetConnectedHulls(includingThis: false, searchDepth: 3, ignoreClosedGaps: true)) + { + //another vent in the connected hull -> don't add it to this vent's total hull volume + if (ventList.Any(v => v.vent != vent && v.vent.Item.CurrentHull == connectedHull)) { continue; } totalHullVolume += connectedHull.Volume; - ventList[vent] += connectedHull.Volume; + ventList[i] = (ventList[i].vent, ventList[i].hullVolume + connectedHull.Volume); } } } - - private void UpdateVents(float deltaOxygen) + + private void UpdateVents(float deltaOxygen, float deltaTime) { - if (ventList == null) + if (ventList == null || ventUpdateTimer < 0.0f) { GetVents(); + ventUpdateTimer = VentUpdateInterval; } + ventUpdateTimer -= deltaTime; if (!ventList.Any() || totalHullVolume <= 0.0f) { return; } - foreach (KeyValuePair v in ventList) + foreach ((Vent vent, float hullVolume) in ventList) { - if (v.Key?.Item.CurrentHull == null) { continue; } + if (vent.Item.CurrentHull == null) { continue; } - v.Key.OxygenFlow = deltaOxygen * (v.Value / totalHullVolume); - v.Key.IsActive = true; + vent.OxygenFlow = deltaOxygen * (hullVolume / totalHullVolume); + vent.IsActive = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 073ae51cf..351e57ef4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -70,6 +70,8 @@ namespace Barotrauma.Items.Components public bool HasPower => IsActive && Voltage >= MinVoltage; public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; + private const float TinkeringSpeedIncrease = 1.5f; + public Pump(Item item, XElement element) : base(item, element) { @@ -105,11 +107,19 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f); currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; + + if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + { + currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; + } + //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); item.CurrentHull.WaterVolume += currFlow; if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } + + Voltage -= deltaTime; } public void InfectBallast(string identifier, bool allowMultiplePerShip = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index e8866afaf..8655b2618 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -367,23 +367,19 @@ namespace Barotrauma.Items.Components item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; } } - - if (item.CurrentHull != null) - { - var aiTarget = item.CurrentHull.AiTarget; - if (aiTarget != null && MaxPowerOutput > 0) - { - float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; - float noise = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); - aiTarget.SoundRange = Math.Max(aiTarget.SoundRange, noise); - } - } - if (item.AiTarget != null && MaxPowerOutput > 0) { var aiTarget = item.AiTarget; float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); + if (item.CurrentHull != null) + { + var hullAITarget = item.CurrentHull.AiTarget; + if (hullAITarget != null) + { + hullAITarget.SoundRange = Math.Max(hullAITarget.SoundRange, aiTarget.SoundRange); + } + } } } @@ -498,15 +494,12 @@ namespace Barotrauma.Items.Components { float prevFireTimer = fireTimer; fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); - - #if SERVER if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item) { - GameMain.Server.KarmaManager.OnReactorOverHeating(blameOnBroken.Character, deltaTime); + GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime); } #endif - if (fireTimer >= FireDelay && prevFireTimer < fireDelay) { new FireSource(item.WorldPosition); @@ -595,7 +588,7 @@ namespace Barotrauma.Items.Components GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction); if (GameMain.Server != null) { - GameMain.Server.KarmaManager.OnReactorMeltdown(blameOnBroken?.Character); + GameMain.Server.KarmaManager.OnReactorMeltdown(item, blameOnBroken?.Character); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 1d0ad14bb..44e6461b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -1,4 +1,4 @@ -using Barotrauma.Networking; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -364,6 +364,16 @@ namespace Barotrauma.Items.Components float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f); item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out"); + // converts the controlled sub's velocity to km/h and sends it. + if (controlledSub is { } sub) + { + item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x"); + item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y"); + + item.SendSignal(new Signal((sub.WorldPosition.X * Physics.DisplayToRealWorldRatio).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); + item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y"); + } + // if our tactical AI pilot has left, revert back to maintaining position if (navigateTactically && (user == null || user.SelectedConstruction != item)) { @@ -382,8 +392,7 @@ namespace Barotrauma.Items.Components float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( "helm", - SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime, - user.Position + Vector2.UnitY * 150.0f); + SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime); } private void UpdateAutoPilot(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 1828f7cee..52c065e75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -15,6 +15,9 @@ namespace Barotrauma.Items.Components //a list of connections a given connection is connected to, either directly or via other power transfer components private readonly Dictionary> connectedRecipients = new Dictionary>(); + private float overloadCooldownTimer; + private const float OverloadCooldown = 5.0f; + protected float powerLoad; protected bool isBroken; @@ -173,12 +176,19 @@ namespace Barotrauma.Items.Components Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { + if (overloadCooldownTimer > 0.0f) + { + overloadCooldownTimer -= deltaTime; + return; + } + //damage the item if voltage is too high (except if running as a client) float prevCondition = item.Condition; item.Condition -= deltaTime * 10.0f; if (item.Condition <= 0.0f && prevCondition > 0.0f) { + overloadCooldownTimer = OverloadCooldown; #if CLIENT SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull); Vector2 baseVel = Rand.Vector(300.0f); @@ -370,5 +380,12 @@ namespace Barotrauma.Items.Components } } } + + protected override void RemoveComponentSpecific() + { + base.RemoveComponentSpecific(); + connectedRecipients?.Clear(); + connectionDirty?.Clear(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 1134861ec..8cc3945de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -137,6 +137,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Can the item stick even to deflective targets.")] + public bool StickToDeflective + { + get; + set; + } + [Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -201,7 +208,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } - Attack = new Attack(subElement, item.Name + ", Projectile"); + Attack = new Attack(subElement, item.Name + ", Projectile", item); } InitProjSpecific(element); } @@ -227,10 +234,14 @@ namespace Barotrauma.Items.Components } } - private void Launch(Character user, Vector2 simPosition, float rotation) + private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f) { Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); + if (Attack != null) + { + Attack.DamageMultiplier = damageMultiplier; + } // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. @@ -243,7 +254,7 @@ namespace Barotrauma.Items.Components Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); } - public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent) + public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f) { //add the limbs of the shooter to the list of bodies to be ignored //so that the player can't shoot himself @@ -264,7 +275,7 @@ namespace Barotrauma.Items.Components projectilePos = newPos; } } - Launch(user, projectilePos, rotation); + Launch(user, projectilePos, rotation, damageMultiplier); if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER @@ -329,7 +340,7 @@ namespace Barotrauma.Items.Components item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; } - item.Drop(null); + item.Drop(null, createNetworkEvent: false); launchPos = item.SimPosition; @@ -355,6 +366,7 @@ namespace Barotrauma.Items.Components { float rotation = item.body.Rotation; Vector2 simPositon = item.SimPosition; + Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); item.body.Enabled = true; @@ -366,7 +378,6 @@ namespace Barotrauma.Items.Components Vector2 rayStart = simPositon; Vector2 rayEnd = rayStart + dir * 500.0f; - Vector2 rayStartWorld = item.WorldPosition; float worldDist = 1000.0f; #if CLIENT worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth; @@ -578,7 +589,8 @@ namespace Barotrauma.Items.Components if (!removePending) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity; + ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user); } if (item.body != null && item.body.FarseerBody.IsBullet) @@ -602,10 +614,17 @@ namespace Barotrauma.Items.Components return; } + //target very far from the item -> update the item's transform to make sure it's inside the same sub as the target (or outside) + if (Math.Abs(stickJoint.JointTranslation) > 100.0f) + { + item.UpdateTransform(); + } + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (StickTargetRemoved() || - (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f))) + (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) || + Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far { Unstick(); #if SERVER @@ -623,7 +642,6 @@ namespace Barotrauma.Items.Components return false; } - private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact) { if (User != null && User.Removed) { User = null; return false; } @@ -694,7 +712,8 @@ namespace Barotrauma.Items.Components } } - readonly List targets = new List(); + private readonly List targets = new List(); + private Fixture lastTarget; private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { @@ -705,6 +724,7 @@ namespace Barotrauma.Items.Components { return false; } + lastTarget = target; float projectileNewSpeed = 0.5f; float projectileDeflectedNewSpeed = 0.1f; @@ -835,14 +855,16 @@ namespace Barotrauma.Items.Components } if (attackResult.AppliedDamageModifiers != null && - attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles)) + (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) { item.body.LinearVelocity *= projectileDeflectedNewSpeed; } - else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && + else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick + //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && + hits.Count() >= MaxTargetsToHit && target.Body.Mass > item.body.Mass * 0.5f && (DoesStick || - (StickToCharacters && target.Body.UserData is Limb) || + (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || (StickToStructures && target.Body.UserData is Structure) || (StickToItems && target.Body.UserData is Item))) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs new file mode 100644 index 000000000..5ffe84c32 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -0,0 +1,91 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Quality : ItemComponent + { + public const int MaxQuality = 3; + + public static readonly float[] QualityCommonnesses = new float[] + { + 0.8f, + 0.15f, + 0.045f, + 0.005f, + }; + + public enum StatType + { + Condition, + ExplosionRadius, + ExplosionDamage, + RepairSpeed, + RepairToolStructureRepairMultiplier, + RepairToolStructureDamageMultiplier, + RepairToolDeattachTimeMultiplier, + // unused as of now + AttackMultiplier, + AttackSpeedMultiplier, + ForceDoorsOpenSpeedMultiplier, + RangedSpreadReduction, + ChargeSpeedMultiplier, + MovementSpeedMultiplier, + // generic stats to be used for various needs, declared just in case (localization) + EffectivenessMultiplier, + PowerOutputMultiplier, + ConsumptionReductionMultiplier, + } + + private readonly Dictionary statValues = new Dictionary(); + + private int qualityLevel; + + [Serialize(0, true)] + public int QualityLevel + { + get { return qualityLevel; } + set + { + if (value == qualityLevel) { return; } + + bool wasInFullCondition = item.IsFullCondition; + qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); + //set the condition to the new max condition + if (wasInFullCondition && statValues.ContainsKey(StatType.Condition)) + { + item.Condition = item.MaxCondition; + } + } + } + + public Quality(Item item, XElement element) : base(item, element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLower()) + { + case "stattype": + case "statvalue": + case "qualitystat": + string statTypeString = subElement.GetAttributeString("stattype", ""); + if (!Enum.TryParse(statTypeString, true, out StatType statType)) + { + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + item.prefab.Identifier + ")"); + } + float statValue = subElement.GetAttributeFloat("value", 0f); + statValues.TryAdd(statType, statValue); + break; + } + } + } + + public float GetValue(StatType statType) + { + if (!statValues.ContainsKey(statType)) { return 0.0f; } + return statValues[statType] * qualityLevel; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs new file mode 100644 index 000000000..783253c89 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -0,0 +1,96 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class RemoteController : ItemComponent + { + [Serialize("", false, description: "Tag or identifier of the item that should be controlled.")] + public string Target + { + get; + private set; + } + + [Serialize(false, false)] + public bool OnlyInOwnSub + { + get; + private set; + } + + [Serialize(10000.0f, false)] + public float Range + { + get; + private set; + } + + public Item TargetItem { get => currentTarget; } + + private Item currentTarget; + private Character currentUser; + private Submarine currentSub; + + public RemoteController(Item item, XElement element) + : base(item, element) + { + } + + public override bool Select(Character character) + { + if (base.Select(character)) + { + FindTarget(character); + return true; + } + return false; + } + + public override void Equip(Character character) + { + FindTarget(character); + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (currentTarget.Removed || + item.Submarine != currentSub || + Vector2.DistanceSquared(currentTarget.WorldPosition, item.WorldPosition) > Range * Range) + { + FindTarget(currentUser); + } + } + + private void FindTarget(Character user) + { + currentTarget = null; + if (user == null || (item.Submarine == null && OnlyInOwnSub)) + { + IsActive = false; + return; + } + + float closestDist = float.PositiveInfinity; + foreach (Item targetItem in Item.ItemList) + { + if (OnlyInOwnSub) + { + if (targetItem.Submarine != item.Submarine) { continue; } + if (targetItem.Submarine.TeamID != user.TeamID) { continue; } + } + if (!targetItem.HasTag(Target) && targetItem.prefab.Identifier != Target) { continue; } + + float distSqr = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (distSqr > Range * Range || distSqr > closestDist) { continue; } + + currentTarget = targetItem; + currentSub = item.Submarine; + closestDist = distSqr; + currentUser = user; + } + IsActive = currentTarget != null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 26a50ebf7..3ff601550 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - private string header; + private readonly string header; private float deteriorationTimer; private float deteriorateAlwaysResetTimer; @@ -85,10 +85,10 @@ namespace Barotrauma.Items.Components } private float skillRequirementMultiplier; - + [Serialize(1.0f, true)] - public float SkillRequirementMultiplier - { + public float SkillRequirementMultiplier + { get { return skillRequirementMultiplier; } set { @@ -100,21 +100,30 @@ namespace Barotrauma.Items.Components RecreateGUI(); } #endif - } + } } + public bool IsTinkering { get; private set; } = false; + public float RepairIconThreshold { get { return RepairThreshold / 2; } } public Character CurrentFixer { get; private set; } + private Item currentRepairItem; + + private float tinkeringDuration; + private float tinkeringStrength; + + public float TinkeringStrength => tinkeringStrength; public enum FixActions : int { None = 0, Repair = 1, - Sabotage = 2 + Sabotage = 2, + Tinker = 3, } private FixActions currentFixerAction = FixActions.None; @@ -131,13 +140,13 @@ namespace Barotrauma.Items.Components canBeSelected = true; this.item = item; - header = + header = TextManager.Get(element.GetAttributeString("header", ""), returnNull: true) ?? TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""), returnNull: true) ?? element.GetAttributeString("name", ""); //backwards compatibility - var repairThresholdAttribute = + var repairThresholdAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)) ?? element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairthreshold", StringComparison.OrdinalIgnoreCase)); if (repairThresholdAttribute != null) @@ -161,12 +170,14 @@ namespace Barotrauma.Items.Components /// /// Check if the character manages to succesfully repair the item /// - public bool CheckCharacterSuccess(Character character) + public bool CheckCharacterSuccess(Character character, Item bestRepairItem) { if (character == null) { return false; } if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { 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.Equals("electrical", StringComparison.OrdinalIgnoreCase)) && item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { return true; } @@ -174,6 +185,10 @@ namespace Barotrauma.Items.Components if (Rand.Range(0.0f, 0.5f) < RepairDegreeOfSuccess(character, requiredSkills)) { return true; } ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) + { + h.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + } return false; } @@ -191,7 +206,7 @@ namespace Barotrauma.Items.Components return ((average + 100.0f) / 2.0f) / 100.0f; } - + public bool StartRepairing(Character character, FixActions action) { if (character == null || character.IsDead || action == FixActions.None) @@ -201,13 +216,19 @@ namespace Barotrauma.Items.Components } else { + Item bestRepairItem = GetBestRepairItem(character); #if SERVER if (CurrentFixer != character || currentFixerAction != action) { - if (!CheckCharacterSuccess(character)) + 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 object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); + if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) + { + GameMain.Server?.CreateEntityEvent(bestRepairItem, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, h, character.ID }); + } + return false; } @@ -215,11 +236,32 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #else - if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; } + if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character, bestRepairItem)) { return false; } #endif CurrentFixer = character; + currentRepairItem = bestRepairItem; CurrentFixerAction = action; + if (action == FixActions.Tinker) + { + tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength); + + if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent() != null || item.GetComponent() != null) + { + // fabricators and deconstructors can be tinkered indefinitely (more or less) + tinkeringDuration = float.MaxValue; + } + else + { + tinkeringDuration = CurrentFixer.GetStatValue(StatTypes.TinkeringDuration); + } + } return true; + + static Item GetBestRepairItem(Character character) + { + return character.HeldItems.OrderByDescending(i => i.Prefab.AddedRepairSpeedMultiplier).FirstOrDefault(); + } + } } @@ -233,12 +275,24 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif + if (currentRepairItem != null) + { + foreach (var ic in currentRepairItem.GetComponents()) + { + ic.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, character); + } + } + if (CurrentFixerAction == FixActions.Tinker) + { + CurrentFixer.CheckTalents(AbilityEffectType.OnStopTinkering); + } CurrentFixer.AnimController.Anim = AnimController.Animation.None; CurrentFixer = null; + currentRepairItem = null; currentFixerAction = FixActions.None; #if CLIENT repairSoundChannel?.FadeOutAndDispose(); - repairSoundChannel = null; + repairSoundChannel = null; #endif return true; } @@ -266,7 +320,10 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(deltaTime); - + IsTinkering = false; + + item.SendSignal($"{(int) item.ConditionPercentage}", "condition_out"); + if (CurrentFixer == null) { if (deteriorateAlwaysResetTimer > 0.0f) @@ -314,6 +371,25 @@ namespace Barotrauma.Items.Components return; } + if (currentFixerAction == FixActions.Tinker) + { + tinkeringDuration -= deltaTime; + // not great to interject it here, should be less reliant on returning + + float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.Prefab.Health) * 100f; + item.Condition -= conditionDecrease; + + if (!CanTinker(CurrentFixer) || tinkeringDuration <= 0f) + { + StopRepairing(CurrentFixer); + } + else + { + IsTinkering = true; + } + return; + } + 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 @@ -327,6 +403,11 @@ namespace Barotrauma.Items.Components } float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); + fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; + fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); + + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + if (currentFixerAction == FixActions.Repair) { if (fixDuration <= 0.0f) @@ -335,7 +416,8 @@ namespace Barotrauma.Items.Components } else { - float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition); + // scale with prefab's health instead of real health to ensure repair speed remains static with upgrades + float conditionIncrease = deltaTime / (fixDuration / item.Prefab.Health); item.Condition += conditionIncrease; #if SERVER GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease); @@ -350,13 +432,13 @@ namespace Barotrauma.Items.Components { float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, - SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f), - CurrentFixer.Position + Vector2.UnitY * 100.0f); + SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); + CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); - wasBroken = false; + wasBroken = false; StopRepairing(CurrentFixer); } } @@ -368,7 +450,8 @@ namespace Barotrauma.Items.Components } else { - float conditionDecrease = deltaTime / (fixDuration / item.MaxCondition); + // scale with prefab's health instead of real health to ensure sabotage speed remains static with (any) upgrades + float conditionDecrease = deltaTime / (fixDuration / item.Prefab.Health); item.Condition -= conditionDecrease; } @@ -380,8 +463,7 @@ namespace Barotrauma.Items.Components { float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier, - SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f), - CurrentFixer.Position + Vector2.UnitY * 100.0f); + SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f)); } deteriorationTimer = 0.0f; @@ -399,6 +481,45 @@ namespace Barotrauma.Items.Components } } + private float GetMaxRepairConditionMultiplier(Character character) + { + 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.Equals("mechanical", StringComparison.OrdinalIgnoreCase))) + { + return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); + } + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + { + return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); + } + return 1.0f; + } + + private bool IsTinkerable(Character character) + { + if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; } + if (item.GetComponent() != null) { return true; } + if (item.GetComponent() != null) { return true; } + if (item.HasTag("turretammosource")) { return true; } + if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; } + if (item.GetComponent() != null) { return true; } + if (item.GetComponent() != null) { return true; } + return false; + } + + private Affliction GetTinkerExhaustion(Character character) + { + return character.CharacterHealth.GetAffliction("tinkerexhaustion"); + } + + private bool CanTinker(Character character) + { + if (!IsTinkerable(character)) { return false; } + if (GetTinkerExhaustion(character) is Affliction tinkerExhaustion && tinkerExhaustion.Strength <= tinkerExhaustion.Prefab.MaxStrength) { return false; } + return true; + } + partial void UpdateProjSpecific(float deltaTime); public void AdjustPowerConsumption(ref float powerConsumption) @@ -423,7 +544,7 @@ namespace Barotrauma.Items.Components } else if (ic is PowerTransfer pt) { - //power transfer items (junction boxes, relays) don't deteriorate if they're no carrying any power + //power transfer items (junction boxes, relays) don't deteriorate if they're no carrying any power if (Math.Abs(pt.CurrPowerConsumption) > 0.1f) { return true; } } else if (ic is Engine engine) @@ -484,7 +605,7 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing - //Repairables should always stay active, so we don't want to use the default behavior + //Repairables should always stay active, so we don't want to use the default behavior //where set_active/set_state signals can disable the component } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 1f923cc23..6e9836551 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -3,7 +3,6 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -54,6 +53,27 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false, description: "Should the rope snap when the character drops the aim?")] + public bool SnapWhenNotAimed + { + get; + set; + } + + [Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] + public float TargetMinMass + { + get; + set; + } + + [Serialize(false, false)] + public bool LerpForces + { + get; + set; + } + private bool snapped; public bool Snapped { @@ -75,6 +95,10 @@ namespace Barotrauma.Items.Components } } snapped = value; + if (!snapped) + { + snapTimer = 0; + } } } @@ -85,6 +109,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); + public void Snap() => Snapped = true; public void Attach(ISpatialEntity source, Item target) { @@ -92,6 +117,7 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(target != null); this.source = source; this.target = target; + Snapped = false; ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; } @@ -118,13 +144,15 @@ namespace Barotrauma.Items.Components Vector2 diff = target.WorldPosition - source.WorldPosition; if (diff.LengthSquared() > MaxLength * MaxLength) { - Snapped = true; + Snap(); return; } #if CLIENT item.ResetCachedVisibleSize(); #endif + var projectile = target.GetComponent(); + if (projectile == null) { return; } if (SnapOnCollision) { @@ -135,28 +163,24 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, customPredicate: (Fixture f) => { - var projectile = target?.GetComponent(); - if (projectile != null) + foreach (Body body in projectile.Hits) { - foreach (Body body in projectile.Hits) + Submarine alreadyHitSub = null; + if (body.UserData is Structure hitStructure) { - Submarine alreadyHitSub = null; - if (body.UserData is Structure hitStructure) - { - alreadyHitSub = hitStructure.Submarine; - } - else if (body.UserData is Submarine hitSub) - { - alreadyHitSub = hitSub; - } - if (alreadyHitSub != null) - { - if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } - if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } - } + alreadyHitSub = hitStructure.Submarine; + } + else if (body.UserData is Submarine hitSub) + { + alreadyHitSub = hitSub; + } + if (alreadyHitSub != null) + { + if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } + if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } } } - Submarine targetSub = target?.GetComponent()?.StickTarget?.UserData as Submarine ?? target.Submarine; + Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine; if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null) { @@ -175,7 +199,7 @@ namespace Barotrauma.Items.Components return true; }) != null) { - Snapped = true; + Snap(); return; } raycastTimer = 0.0f; @@ -183,27 +207,107 @@ namespace Barotrauma.Items.Components } Vector2 forceDir = diff; - if (forceDir.LengthSquared() > 0.01f) + float distance = diff.Length(); + if (distance > 0.001f) { forceDir = Vector2.Normalize(forceDir); } if (Math.Abs(ProjectilePullForce) > 0.001f) { - var projectile = target.GetComponent(); - projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); + projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); } - if (Math.Abs(SourcePullForce) > 0.001f) + if (projectile.StickTarget != null) { - var sourceBody = GetBodyToPull(source); - sourceBody?.ApplyForce(forceDir * SourcePullForce); - } - - if (Math.Abs(TargetPullForce) > 0.001f) - { - var targetBody = GetBodyToPull(target); - targetBody?.ApplyForce(-forceDir * TargetPullForce); + float targetMass = float.MaxValue; + Character targetCharacter = null; + if (projectile.StickTarget.UserData is Limb targetLimb) + { + 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; + } + if (projectile.StickTarget.BodyType != BodyType.Dynamic) + { + targetMass = float.MaxValue; + } + var user = item.GetComponent()?.User; + if (targetMass > TargetMinMass) + { + if (Math.Abs(SourcePullForce) > 0.001f) + { + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + var targetBody = GetBodyToPull(target); + if (targetBody != null && !(targetBody.UserData is Character)) + { + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); + } + float forceMultiplier = 1; + if (user != null) + { + user.AnimController.Hang(); + if (user.InWater) + { + if (user.IsRagdolled) + { + forceMultiplier = 0; + } + } + else + { + forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f; + // Prevents too easy smashing to the walls + forceDir.X /= 4; + // Prevents rubberbanding up and down + if (forceDir.Y < 0) + { + forceDir.Y = 0; + } + } + if (targetCharacter != null) + { + var myCollider = user.AnimController.Collider; + var targetCollider = targetCharacter.AnimController.Collider; + if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero) + { + if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0) + { + myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass); + } + } + } + } + float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier; + sourceBody.ApplyForce(forceDir * force); + } + } + } + if (Math.Abs(TargetPullForce) > 0.001f) + { + var targetBody = GetBodyToPull(target); + if (user != null && targetCharacter != null && !user.AnimController.InWater) + { + // Prevents rubberbanding horizontally when dragging a corpse. + if ((forceDir.X < 0) != (user.AnimController.Dir < 0)) + { + forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f); + } + } + float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce; + targetBody?.ApplyForce(-forceDir * force); + targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3); + } } } @@ -231,19 +335,23 @@ namespace Barotrauma.Items.Components return ownerCharacter.AnimController.Collider; } var projectile = targetItem.GetComponent(); - if (projectile != null) + if (projectile != null && projectile.StickTarget != null) { - if (projectile.StickTarget?.UserData is Structure structure) + if (projectile.StickTarget.UserData is Structure structure) { return structure.Submarine?.PhysicsBody; } - else if (projectile.StickTarget?.UserData is Submarine sub) + else if (projectile.StickTarget.UserData is Submarine sub) { - return sub?.PhysicsBody; + return sub.PhysicsBody; } - else if (projectile.StickTarget?.UserData is Character character) + else if (projectile.StickTarget.UserData is Item item) { - return character.AnimController.Collider; + return item.body; + } + else if (projectile.StickTarget.UserData is Limb limb) + { + return limb.body; } return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..9c4998801 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -0,0 +1,90 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent + { + [Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")] + public float ScanDuration { get; set; } + [Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] + public float ScanTimer + { + get + { + return scanTimer; + } + set + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (Holdable == null) { return; } + bool wasScanCompletedPreviously = IsScanCompleted; + scanTimer = Math.Max(0.0f, value); + if (!wasScanCompletedPreviously && IsScanCompleted) + { + OnScanCompleted?.Invoke(this); + } +#if SERVER + if (wasScanCompletedPreviously != IsScanCompleted || Math.Abs(LastSentScanTimer - scanTimer) > 0.1f) + { + item.CreateServerEvent(this); + LastSentScanTimer = scanTimer; + } +#endif + } + } + [Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")] + public float ScanRadius { get; set; } + [Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")] + public bool AlwaysDisplayProgressBar { get; set; } + + private Holdable Holdable { get; set; } + /// + /// Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false. + /// + public bool DisplayProgressBar { get; set; } = false; + private bool IsScanCompleted => scanTimer >= ScanDuration; + + private float scanTimer; + + public Action OnScanStarted, OnScanCompleted; + + public Scanner(Item item, XElement element) : base(item, element) + { + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + if (Holdable != null && Holdable.Attachable && Holdable.Attached) + { + if (ScanTimer <= 0.0f) + { + OnScanStarted?.Invoke(this); + } + ScanTimer += deltaTime; + item.AiTarget?.IncreaseSoundRange(deltaTime, speed: 2.0f); + ApplyStatusEffects(ActionType.OnActive, deltaTime); + } + else + { + ScanTimer = 0.0f; + DisplayProgressBar = false; + } + UpdateProjSpecific(); + } + + partial void UpdateProjSpecific(); + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + Holdable = item.GetComponent(); + if (Holdable == null || !Holdable.Attachable) + { + DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found"); + IsActive = false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..88883ccca --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,119 @@ +using Barotrauma.Extensions; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent + { + [Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] + public string[] Signals { get; set; } + [Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] + public string ActivatingItems { get; set; } + + private int RequiredSignalCount { get; set; } + private ItemContainer Container { get; set; } + private HashSet ActivatingItemPrefabs { get; set; } = new HashSet(); + + + private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)); + + public ButtonTerminal(Item item, XElement element) : base(item, element) + { + IsActive = true; + RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); + if (RequiredSignalCount < 1) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!"); + } + InitProjSpecific(element); + } + + partial void InitProjSpecific(XElement element); + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + + if (Signals == null) + { + Signals = new string[RequiredSignalCount]; + for (int i = 0; i < RequiredSignalCount; i++) + { + Signals[i] = string.Empty; + } + } + else if (Signals.Length != RequiredSignalCount) + { + string[] newSignals = new string[RequiredSignalCount]; + if (Signals.Length < RequiredSignalCount) + { + Signals.CopyTo(newSignals, 0); + for (int i = Signals.Length; i < RequiredSignalCount; i++) + { + newSignals[i] = string.Empty; + } + } + else + { + for (int i = 0; i < RequiredSignalCount; i++) + { + newSignals[i] = Signals[i]; + } + } + Signals = newSignals; + } + + ActivatingItemPrefabs.Clear(); + if (!string.IsNullOrEmpty(ActivatingItems)) + { + foreach (var activatingItem in ActivatingItems.Split(',')) + { + if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab) + { + ActivatingItemPrefabs.Add(prefab); + } + else + { + ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase))) + .ForEach(p => ActivatingItemPrefabs.Add(p)); + } + } + if (ActivatingItemPrefabs.None()) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\""); + } + } + + var containers = item.GetComponents().ToList(); + if (containers.Count != 1) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!"); + return; + } + Container = containers[0]; + + OnItemLoadedProjSpecific(); + } + + partial void OnItemLoadedProjSpecific(); + + private bool SendSignal(int signalIndex, bool isServerMessage = false) + { + if (!isServerMessage && !AllowUsingButtons) { return false; } + string signal = Signals[signalIndex]; + string connectionName = $"signal_out{signalIndex + 1}"; + item.SendSignal(signal, connectionName); + return true; + } + + private void Write(IWriteMessage msg, object[] extraData) + { + if (extraData == null || extraData.Length < 3) { return; } + msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 09339aabc..90db70255 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Items.Components foreach (XElement connectionElement in subElement.Elements()) { - string prefabConnectionName = element.GetAttributeString("name", null); + string prefabConnectionName = connectionElement.GetAttributeString("name", null); if (prefabConnectionName == Name) { displayNameTag = connectionElement.GetAttributeString("displayname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 38c56e427..a52a9600b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -134,20 +134,30 @@ namespace Barotrauma.Items.Components foreach (Wire wire in c.Wires) { if (wire == null) { continue; } -#if CLIENT - if (wire.Item.IsSelected) { continue; } -#endif - var wireNodes = wire.GetNodes(); - if (wireNodes.Count == 0) { continue; } + TryMoveWire(wire); + } + } - if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset)) - { - wire.MoveNode(0, amount); - } - else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset)) - { - wire.MoveNode(wireNodes.Count - 1, amount); - } + foreach (var wire in DisconnectedWires) + { + TryMoveWire(wire); + } + + void TryMoveWire(Wire wire) + { +#if CLIENT + if (wire.Item.IsSelected) { return; } +#endif + var wireNodes = wire.GetNodes(); + if (wireNodes.Count == 0) { return; } + + if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset)) + { + wire.MoveNode(0, amount); + } + else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset)) + { + wire.MoveNode(wireNodes.Count - 1, amount); } } } @@ -350,6 +360,7 @@ namespace Barotrauma.Items.Components } } } + Connections.Clear(); #if CLIENT rewireSoundChannel?.FadeOutAndDispose(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index a836fab59..90814c3cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -159,7 +159,10 @@ namespace Barotrauma.Items.Components { lightColor = value; #if CLIENT - if (Light != null) Light.Color = IsActive ? lightColor : Color.Transparent; + if (Light != null) + { + Light.Color = IsActive ? lightColor : Color.Transparent; + } #endif } } @@ -233,6 +236,8 @@ namespace Barotrauma.Items.Components } UpdateOnActiveEffects(deltaTime); + if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } + #if CLIENT Light.ParentSub = item.Submarine; #endif @@ -293,8 +298,6 @@ namespace Barotrauma.Items.Components } SetLightSourceState(true, lightBrightness); - - if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } } public override void UpdateBroken(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 309bfee73..6d92474fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -12,8 +12,10 @@ namespace Barotrauma.Items.Components public enum WaveType { Pulse, + Sawtooth, Sine, Square, + Triangle, } private float frequency; @@ -22,8 +24,11 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + + " Sawtooth: sends out a periodic wave that increases linearly from 0 to 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + - " Square: sends out a signal that alternates between 0 and 1.", alwaysUseInstanceValues: true)] + " Square: sends out a signal that alternates between 0 and 1." + + " Triangle: sends out a wave that alternates between increasing linearly from -1 to 1 and decreasing from 1 to -1.", + alwaysUseInstanceValues: true)] public WaveType OutputType { get; @@ -63,6 +68,10 @@ namespace Barotrauma.Items.Components phase -= pulseInterval; } break; + case WaveType.Sawtooth: + phase = (phase + deltaTime * frequency) % 1.0f; + item.SendSignal(phase.ToString(CultureInfo.InvariantCulture), "signal_out"); + break; case WaveType.Square: phase = (phase + deltaTime * frequency) % 1.0f; item.SendSignal(phase < 0.5f ? "0" : "1", "signal_out"); @@ -71,6 +80,11 @@ namespace Barotrauma.Items.Components phase = (phase + deltaTime * frequency) % 1.0f; item.SendSignal(Math.Sin(phase * MathHelper.TwoPi).ToString(CultureInfo.InvariantCulture), "signal_out"); break; + case WaveType.Triangle: + phase = (phase + deltaTime * frequency) % 1.0f; + float output = 4.0f * MathF.Abs(MathUtils.PositiveModulo(phase - 0.25f, 1.0f) - 0.5f) - 1.0f; + item.SendSignal(output.ToString(CultureInfo.InvariantCulture), "signal_out"); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index b2f512ca2..d19349597 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -80,19 +80,21 @@ namespace Barotrauma.Items.Components public RegExFindComponent(Item item, XElement element) : base(item, element) { + nonContinuousOutputSent = true; IsActive = true; } public override void Update(float deltaTime, Camera cam) { - if (string.IsNullOrWhiteSpace(expression) || regex == null) return; + if (string.IsNullOrWhiteSpace(expression) || regex == null) { return; } + if (!ContinuousOutput && nonContinuousOutputSent) { return; } if (receivedSignal != previousReceivedSignal && receivedSignal != null) { try { Match match = regex.Match(receivedSignal); - previousResult = match.Success; + previousResult = match.Success; previousGroups = UseCaptureGroup && previousResult ? match.Groups : null; previousReceivedSignal = receivedSignal; @@ -133,7 +135,7 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } } - else if (!nonContinuousOutputSent) + else { if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); } nonContinuousOutputSent = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index fb93543ab..335827046 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -41,10 +41,13 @@ namespace Barotrauma.Items.Components set { if (string.IsNullOrEmpty(value)) { return; } - ShowOnDisplay(value); + ShowOnDisplay(value, addToHistory: true); } } + [Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] + public bool UseMonospaceFont { get; set; } + private string OutputValue { get; set; } public Terminal(Item item, XElement element) @@ -56,7 +59,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input, bool addToHistory = true); + partial void ShowOnDisplay(string input, bool addToHistory); public override void ReceiveSignal(Signal signal, Connection connection) { @@ -67,14 +70,14 @@ namespace Barotrauma.Items.Components } string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal); + ShowOnDisplay(inputSignal, addToHistory: true); } public override void OnItemLoaded() { bool isSubEditor = false; #if CLIENT - isSubEditor = Screen.Selected != GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode; + isSubEditor = Screen.Selected == GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode; #endif base.OnItemLoaded(); @@ -107,7 +110,7 @@ namespace Barotrauma.Items.Components { string msg = componentElement.GetAttributeString("msg" + i, null); if (msg == null) { break; } - ShowOnDisplay(msg); + ShowOnDisplay(msg, addToHistory: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 7c6c61021..763800065 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -100,9 +100,11 @@ namespace Barotrauma.Items.Components if (item.CurrentHull != null) { - int waterPercentage = MathHelper.Clamp((int)Math.Round(item.CurrentHull.WaterPercentage), 0, 100); + int waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); item.SendSignal(waterPercentage.ToString(), "water_%"); } + string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0"; + item.SendSignal(highPressureOut, "high_pressure"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index ad06fd954..e768c3faa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -198,7 +198,7 @@ namespace Barotrauma.Items.Components //signal strength diminishes by distance float sentSignalStrength = signal.strength * MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f); - Signal s = new Signal(signal.value, ++signal.stepsTaken, sender: signal.sender, source: signal.source, + Signal s = new Signal(signal.value, signal.stepsTaken + 1, sender: signal.sender, source: signal.source, power: 0.0f, strength: sentSignalStrength); if (wifiComp.signalOutConnection != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 3a054c4e1..2b80a81c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -133,15 +133,15 @@ namespace Barotrauma.Items.Components public bool IsConnectedTo(Item item) { - if (connections[0] != null && connections[0].Item == item) return true; - return (connections[1] != null && connections[1].Item == item); + if (connections[0] != null && connections[0].Item == item) { return true; } + return connections[1] != null && connections[1].Item == item; } public void RemoveConnection(Item item) { for (int i = 0; i < 2; i++) { - if (connections[i] == null || connections[i].Item != item) continue; + if (connections[i] == null || connections[i].Item != item) { continue; } foreach (Wire wire in connections[i].Wires) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs new file mode 100644 index 000000000..10ef02bdb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -0,0 +1,187 @@ +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using FarseerPhysics.Dynamics.Contacts; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class TriggerComponent : ItemComponent + { + [Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] + public float Force { get; set; } + + public PhysicsBody PhysicsBody { get; private set; } + private float Radius { get; set; } + private float RadiusInDisplayUnits { get; set; } + private bool TriggeredOnce { get; set; } + + public bool TriggerActive { get; private set; } + + private readonly LevelTrigger.TriggererType triggeredBy; + private readonly HashSet triggerers = new HashSet(); + private readonly bool triggerOnce; + private readonly List statusEffectTargets = new List(); + /// + /// Effects applied to entities inside the trigger + /// + private readonly List statusEffects = new List(); + /// + /// Attacks applied to entities inside the trigger + /// + private readonly List attacks = new List(); + + public TriggerComponent(Item item, XElement element) : base(item, element) + { + string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); + if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) + { + DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); + } + triggerOnce = element.GetAttributeBool("triggeronce", false); + string parentDebugName = $"TriggerComponent in {item.Name}"; + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName); + break; + case "attack": + case "damage": + LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks); + break; + } + } + IsActive = true; + } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f); + Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale); + PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f) + { + BodyType = BodyType.Static, + CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy), + CollisionCategories = Physics.CollisionWall, + UserData = item + }; + PhysicsBody.FarseerBody.SetIsSensor(true); + PhysicsBody.FarseerBody.OnCollision += OnCollision; + PhysicsBody.FarseerBody.OnSeparation += OnSeparation; + RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius); + } + + public override void OnMapLoaded() + { + base.OnMapLoaded(); + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + + private bool OnCollision(Fixture sender, Fixture other, Contact contact) + { + if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } + if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; } + triggerers.Add(entity); + return true; + } + + private void OnSeparation(Fixture sender, Fixture other, Contact contact) + { + if (!(LevelTrigger.GetEntity(other) is Entity entity)) + { + return; + } + if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity)) + { + triggerers.Remove(entity); + return; + } + if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity)) + { + return; + } + triggerers.Remove(entity); + } + + public override void Update(float deltaTime, Camera cam) + { + triggerers.RemoveWhere(t => t.Removed); + LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition); + + if (triggerOnce) + { + if (TriggeredOnce) { return; } + if (triggerers.Count > 0) + { + TriggeredOnce = true; + IsActive = false; + triggerers.Clear(); + } + } + + TriggerActive = triggerers.Any(); + + foreach (Entity triggerer in triggerers) + { + LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets); + + if (triggerer is IDamageable damageable) + { + LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime); + } + else if (triggerer is Submarine submarine) + { + LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); + } + + if (Force < 0.01f) + { + // Just ignore very minimal forces + continue; + } + else if (triggerer is Character c) + { + ApplyForce(c.AnimController.Collider); + } + else if (triggerer is Submarine s) + { + ApplyForce(s.SubBody.Body); + } + else if (triggerer is Item i && i.body != null) + { + ApplyForce(i.body); + } + } + + item.SendSignal(IsActive ? "1" : "0", "state_out"); + } + + private void ApplyForce(PhysicsBody body) + { + Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); + if (diff.LengthSquared() < 0.0001f) { return; } + float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits); + if (distanceFactor <= 0.0f) { return; } + Vector2 force = distanceFactor * Force * Vector2.Normalize(diff); + if (force.LengthSquared() < 0.01f) { return; } + body.ApplyForce(force); + } + + public override void Move(Vector2 amount) + { + base.Move(amount); + if (PhysicsBody != null) + { + PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 3ea88ae11..327c23181 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -64,6 +64,10 @@ namespace Barotrauma.Items.Components private Character currentTarget; const float aiFindTargetInterval = 5.0f; + private const float TinkeringPowerCostReduction = 0.2f; + private const float TinkeringDamageIncrease = 0.2f; + private const float TinkeringReloadDecrease = 0.2f; + public float Rotation { get { return rotation; } @@ -381,6 +385,10 @@ namespace Barotrauma.Items.Components else { float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime; + if (chargeDeltaTime > 0f && user != null) + { + chargeDeltaTime *= 1f + user.GetStatValue(StatTypes.TurretChargeSpeed); + } currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime); } tryingToCharge = false; @@ -430,8 +438,7 @@ namespace Barotrauma.Items.Components if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost)) { user.Info.IncreaseSkillLevel("weapons", - SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f), - user.Position + Vector2.UnitY * 150.0f); + SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); } float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); @@ -504,9 +511,19 @@ namespace Barotrauma.Items.Components return TryLaunch(deltaTime, character); } + public float GetPowerRequiredToShoot() + { + float powerCost = powerConsumption; + if (user != null) + { + powerCost /= (1 + user.GetStatValue(StatTypes.TurretPowerCostReduction)); + } + return powerCost; + } + public bool HasPowerToShoot() { - return GetAvailableBatteryPower() >= powerConsumption; + return GetAvailableBatteryPower() >= GetPowerRequiredToShoot(); } private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) @@ -544,6 +561,8 @@ namespace Barotrauma.Items.Components Projectile launchedProjectile = null; bool loaderBroken = false; + float tinkeringStrength = 0f; + for (int i = 0; i < ProjectileCount; i++) { var projectiles = GetLoadedProjectiles(); @@ -575,6 +594,7 @@ namespace Barotrauma.Items.Components projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { break; } } + } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -601,10 +621,25 @@ namespace Barotrauma.Items.Components return false; } failedLaunchAttempts = 0; + + foreach (MapEntity e in item.linkedTo) + { + if (!(e is Item linkedItem)) { continue; } + if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) + { + tinkeringStrength = repairable.TinkeringStrength; + } + } + if (!ignorePower) { var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; + float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting + // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later + neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); + while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); @@ -622,7 +657,8 @@ namespace Barotrauma.Items.Components } launchedProjectile = projectiles.FirstOrDefault(); - if (launchedProjectile?.Item.Container != null) + Item container = launchedProjectile?.Item.Container; + if (container != null) { var repairable = launchedProjectile?.Item.Container.GetComponent(); if (repairable != null) @@ -637,18 +673,22 @@ namespace Barotrauma.Items.Components { foreach (Projectile projectile in projectiles) { - Launch(projectile.Item, character); + Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength); } } else { - Launch(null, character); + Launch(null, character, tinkeringStrength: tinkeringStrength); } if (item.AiTarget != null) { item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; // Turrets also have a light component, which handles the sight range. } + if (container != null) + { + ShiftItemsInProjectileContainer(container.GetComponent()); + } } } @@ -672,9 +712,15 @@ namespace Barotrauma.Items.Components return true; } - private void Launch(Item projectile, Character user = null, float? launchRotation = null) + private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { reload = reloadTime; + reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease); + + if (user != null) + { + reload /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed); + } if (projectile != null) { @@ -697,7 +743,9 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { - projectileComponent.Attacker = user; + projectileComponent.Attacker = projectileComponent.User = user; + projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength); + projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -725,6 +773,26 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); + private void ShiftItemsInProjectileContainer(ItemContainer container) + { + if (container == null) { return; } + bool moved; + do + { + moved = false; + for (int i = 1; i < container.Capacity; i++) + { + if (container.Inventory.GetItemAt(i) is Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1)) + { + if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true)) + { + moved = true; + } + } + } + } while (moved); + } + private float waitTimer; private float disorderTimer; @@ -864,57 +932,26 @@ namespace Barotrauma.Items.Components float turretAngle = -rotation; if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } } - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. + Body worldTarget = CheckLineOfSight(start, end); + bool shoot; if (target.Submarine != null) { start -= target.Submarine.SimPosition; end -= target.Submarine.SimPosition; - } - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => - { - if (f.UserData is Item i && i.GetComponent() != null) { return false; } - return !item.StaticFixtures.Contains(f); - }); - if (pickedBody == null) { return; } - Character targetCharacter = null; - if (pickedBody.UserData is Character c) - { - targetCharacter = c; - } - else if (pickedBody.UserData is Limb limb) - { - targetCharacter = limb.character; - } - if (targetCharacter != null) - { - if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) - { - // Don't shoot friendly characters - return; - } + Body transformedTarget = CheckLineOfSight(start, end); + shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines)); } else { - if (pickedBody.UserData is ISpatialEntity e) - { - Submarine sub = e.Submarine; - if (sub == null) { return; } - if (!targetSubmarines) { return; } - if (sub == Item.Submarine) { return; } - // Don't shoot non-player submarines, i.e. wrecks or outposts. - if (!sub.Info.IsPlayer) { return; } - } - else - { - // Hit something else, probably a level wall - return; - } + shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines); + } + if (shoot) + { + TryLaunch(deltaTime, ignorePower: true); } - TryLaunch(deltaTime, ignorePower: true); } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) @@ -998,7 +1035,7 @@ namespace Barotrauma.Items.Components container = containerItem.GetComponent(); if (container != null) { break; } } - if (container == null || container.ContainableItems.Count == 0) + if (container == null || !container.ContainableItemIdentifiers.Any()) { if (character.IsOnPlayerTeam) { @@ -1022,7 +1059,7 @@ namespace Barotrauma.Items.Components { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - string ammoType = container.ContainableItems.First().Identifiers.FirstOrDefault() ?? "ammobox"; + string ammoType = container.ContainableItemIdentifiers.FirstOrDefault() ?? "ammobox"; int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { @@ -1067,7 +1104,8 @@ namespace Barotrauma.Items.Components { // Ignore dead, friendly, and those that are inside the same sub if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - // Don't aim monsters that are inside a submarine. + if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; } + // Don't aim monsters that are inside any submarine. if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } if (HumanAIController.IsFriendly(character, enemy)) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); @@ -1091,26 +1129,34 @@ namespace Barotrauma.Items.Components if (closestEnemy != null) { - // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. targetPos = closestEnemy.WorldPosition; - float closestDist = closestDistance; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine) { - if (limb.IsSevered) { continue; } - if (limb.Hidden) { continue; } - if (!CheckTurretAngle(limb.WorldPosition)) { continue; } - float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); - if (dist < closestDist) - { - closestDist = dist; - targetPos = limb.WorldPosition; - } + targetPos = closestEnemy.CurrentHull.WorldPosition; } - if (closestDist > shootDistance * shootDistance) + else { - // Not close enough to shoot - closestEnemy = null; - targetPos = null; + // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. + float closestDist = closestDistance; + foreach (Limb limb in closestEnemy.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } + if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + targetPos = limb.WorldPosition; + } + } + if (closestDist > shootDistance * shootDistance) + { + // Not close enough to shoot + closestEnemy = null; + targetPos = null; + } } } else if (item.Submarine != null && Level.Loaded != null) @@ -1158,7 +1204,7 @@ namespace Barotrauma.Items.Components continue; } // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); ; + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); if (dist > closestDistance) { continue; } targetPos = closestPoint; closestDistance = dist; @@ -1222,58 +1268,25 @@ namespace Barotrauma.Items.Components if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; } - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); - if (closestEnemy != null && closestEnemy.Submarine != null) - { - start -= closestEnemy.Submarine.SimPosition; - end -= closestEnemy.Submarine.SimPosition; - } - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => - { - if (f.UserData is Item i && i.GetComponent() != null) { return false; } - return !item.StaticFixtures.Contains(f); - }); - if (pickedBody == null) { return false; } - Character targetCharacter = null; - if (pickedBody.UserData is Character c) - { - targetCharacter = c; - } - else if (pickedBody.UserData is Limb limb) - { - targetCharacter = limb.character; - } - if (targetCharacter != null) - { - if (HumanAIController.IsFriendly(character, targetCharacter)) - { - // Don't shoot friendly characters - return false; - } - } - else - { - if (pickedBody.UserData is ISpatialEntity e) - { - Submarine sub = e.Submarine; - if (sub == null) { return false; } - if (sub == Item.Submarine) { return false; } - // Don't shoot non-player submarines, i.e. wrecks or outposts. - if (!sub.Info.IsPlayer) { return false; } - // Don't shoot friendly submarines. - if (sub.TeamID == Item.Submarine.TeamID) { return false; } - } - else if (!(pickedBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) - { - // Hit something else, probably a level wall - return false; - } - } if (canShoot) { + Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); + Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); + // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target. + Body worldTarget = CheckLineOfSight(start, end); + bool shoot; + if (closestEnemy != null && closestEnemy.Submarine != null) + { + start -= closestEnemy.Submarine.SimPosition; + end -= closestEnemy.Submarine.SimPosition; + Body transformedTarget = CheckLineOfSight(start, end); + shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character)); + } + else + { + shoot = CanShoot(worldTarget, character); + } + if (!shoot) { return false; } if (character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); @@ -1284,6 +1297,67 @@ namespace Barotrauma.Items.Components return false; } + private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + { + if (targetBody == null) { return false; } + Character targetCharacter = null; + if (targetBody.UserData is Character c) + { + targetCharacter = c; + } + else if (targetBody.UserData is Limb limb) + { + targetCharacter = limb.character; + } + if (targetCharacter != null) + { + if (user != null) + { + if (HumanAIController.IsFriendly(user, targetCharacter)) + { + return false; + } + } + if (ai != null) + { + if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + else + { + if (targetBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine ?? e as Submarine; + if (!targetSubmarines && e is Submarine) { return false; } + if (sub == null) { return false; } + if (sub == Item.Submarine) { return false; } + if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } + if (sub.TeamID == Item.Submarine.TeamID) { return false; } + } + else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + { + // Hit something else, probably a level wall + return false; + } + } + return true; + } + + private Body CheckLineOfSight(Vector2 start, Vector2 end) + { + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, + customPredicate: (Fixture f) => + { + if (f.UserData is Item i && i.GetComponent() != null) { return false; } + return !item.StaticFixtures.Contains(f); + }); + return pickedBody; + } + private Vector2 GetRelativeFiringPosition(bool useOffset = true) { Vector2 transformedFiringOffset = Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index b541c4812..2b4190bb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -5,8 +5,8 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; -using Barotrauma.Extensions; using Barotrauma.Networking; +using Barotrauma.Abilities; namespace Barotrauma { @@ -46,14 +46,23 @@ namespace Barotrauma public LimbType Limb { get; private set; } public bool HideLimb { get; private set; } public bool HideOtherWearables { get; private set; } + public bool CanBeHiddenByOtherWearables { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } - public bool InheritTextureScale { get; private set; } + /// + /// Does the wearable inherit all the scalings of the wearer? Also the wearable's own scale is used! + /// + public bool InheritScale { get; private set; } + public bool IgnoreRagdollScale { get; private set; } + public bool IgnoreLimbScale { get; private set; } + public bool IgnoreTextureScale { get; private set; } public bool InheritOrigin { get; private set; } public bool InheritSourceRect { get; private set; } public float Scale { get; private set; } + public float Rotation { get; private set; } + public LimbType DepthLimb { get; private set; } private Wearable _wearableComponent; public Wearable WearableComponent @@ -110,10 +119,9 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - HideLimb = type == WearableType.Husk || type == WearableType.Herpes; HideOtherWearables = false; InheritLimbDepth = true; - InheritTextureScale = true; + InheritScale = true; InheritOrigin = true; InheritSourceRect = true; break; @@ -169,13 +177,27 @@ namespace Barotrauma Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false); + CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); - InheritTextureScale = SourceElement.GetAttributeBool("inherittexturescale", false); + var scale = SourceElement.GetAttribute("inheritscale"); + if (scale != null) + { + InheritScale = scale.GetAttributeBool(false); + } + else + { + InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false); + } + IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false); + IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false); + IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false); + SourceElement.GetAttributeBool("inherittexturescale", false); InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false); InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); Scale = SourceElement.GetAttributeFloat("scale", 1.0f); + Rotation = MathHelper.ToRadians(SourceElement.GetAttributeFloat("rotation", 0.0f)); var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1)); if (index.X > -1 && index.Y > -1) { @@ -210,7 +232,9 @@ namespace Barotrauma.Items.Components private readonly Limb[] limb; private readonly List damageModifiers; - public readonly Dictionary SkillModifiers; + public readonly Dictionary SkillModifiers = new Dictionary(); + + public readonly Dictionary WearableStatValues = new Dictionary(); public IEnumerable DamageModifiers { @@ -266,7 +290,6 @@ namespace Barotrauma.Items.Components this.item = item; damageModifiers = new List(); - SkillModifiers = new Dictionary(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); @@ -280,7 +303,7 @@ namespace Barotrauma.Items.Components int i = 0; foreach (XElement subElement in element.Elements()) { - switch (subElement.Name.ToString().ToLower()) + switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": if (subElement.Attribute("texture") == null) @@ -322,6 +345,18 @@ namespace Barotrauma.Items.Components SkillModifiers.TryAdd(skillIdentifier, skillValue); } break; + case "statvalue": + StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name); + float statValue = subElement.GetAttributeFloat("value", 0f); + if (WearableStatValues.ContainsKey(statType)) + { + WearableStatValues[statType] += statValue; + } + else + { + WearableStatValues.TryAdd(statType, statValue); + } + break; } } } @@ -334,6 +369,7 @@ namespace Barotrauma.Items.Components } picker = character; + for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; @@ -379,19 +415,19 @@ namespace Barotrauma.Items.Components return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); }); } - #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif } + character.OnWearablesChanged(); } public override void Drop(Character dropper) { + Character previousPicker = picker; Unequip(picker); - base.Drop(dropper); - + previousPicker?.OnWearablesChanged(); picker = null; IsActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 3602260ca..2b1b67724 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -48,8 +48,8 @@ namespace Barotrauma return false; } } - if (items[0].Prefab.Identifier != item.Prefab.Identifier || - items.Count + 1 > item.Prefab.MaxStackSize) + if (items[0].Quality != item.Quality) { return false; } + if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize) { return false; } @@ -57,7 +57,7 @@ namespace Barotrauma return true; } - public bool CanBePut(ItemPrefab itemPrefab, float? condition = null) + public bool CanBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null) { if (itemPrefab == null) { return false; } if (items.Count > 0) @@ -82,6 +82,11 @@ namespace Barotrauma if (items.Any(it => !it.IsFullCondition)) { return false; } } + if (quality.HasValue) + { + if (items[0].Quality != quality.Value) { return false; } + } + if (items[0].Prefab.Identifier != itemPrefab.Identifier || items.Count + 1 > itemPrefab.MaxStackSize) { @@ -172,6 +177,11 @@ namespace Barotrauma items.Clear(); } + public void RemoveWhere(Func predicate) + { + items.RemoveAll(it => predicate(it)); + } + public bool Any() { return items.Count > 0; @@ -258,6 +268,8 @@ namespace Barotrauma get { return capacity; } } + public bool AllowSwappingContainedItems = true; + public Inventory(Entity owner, int capacity, int slotsPerRow = 5) { this.capacity = capacity; @@ -420,16 +432,16 @@ namespace Barotrauma return slots[i].CanBePut(item, ignoreCondition); } - public bool CanBePut(ItemPrefab itemPrefab, float? condition = null) + public bool CanBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null) { for (int i = 0; i < capacity; i++) { - if (CanBePutInSlot(itemPrefab, i, condition)) { return true; } + if (CanBePutInSlot(itemPrefab, i, condition, quality)) { return true; } } return false; } - public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null) + public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null) { if (i < 0 || i >= slots.Length) { return false; } return slots[i].CanBePut(itemPrefab, condition); @@ -496,12 +508,12 @@ namespace Barotrauma var itemInSlot = slots[i].First(); if (itemInSlot.OwnInventory != null && !itemInSlot.OwnInventory.Contains(item) && - (itemInSlot.GetComponent()?.MaxStackSize ?? 0) == 1 && + (itemInSlot.GetComponent()?.GetMaxStackSize(0) ?? 0) == 1 && itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false)) { return true; } - return + return TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: true) || TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: false); } @@ -628,6 +640,8 @@ namespace Barotrauma protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent, bool swapWholeStack) { if (item?.ParentInventory == null || !slots[index].Any()) { return false; } + if (slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; } + if (!AllowSwappingContainedItems) { return false; } var should = new LuaResult(GameMain.Lua.hook.Call("inventoryItemSwap", new object[] { this, item, user, index, swapWholeStack })); @@ -782,11 +796,17 @@ namespace Barotrauma { for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item)) { slots[j].RemoveAllItems(); }; + if (slots[j].Contains(item)) + { + slots[j].RemoveWhere(it => existingItems.Contains(it) || stackedItems.Contains(it)); + } } for (int j = 0; j < otherInventory.capacity; j++) { - if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.slots[j].RemoveAllItems(); } + if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) + { + otherInventory.slots[j].RemoveWhere(it => existingItems.Contains(it) || stackedItems.Contains(it)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2d6a462eb..8ddd1506a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -12,6 +12,7 @@ using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; using MoonSharp.Interpreter; +using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -37,7 +38,6 @@ namespace Barotrauma set { currentHull = value; - ParentRuin = currentHull?.ParentRuin; } } @@ -98,14 +98,16 @@ namespace Barotrauma private Dictionary connections; - private List repairables; + private readonly List repairables; - private Queue impactQueue = new Queue(); + private Quality qualityComponent; + + private readonly Queue impactQueue = new Queue(); //a dictionary containing lists of the status effects in all the components of the item - private bool[] hasStatusEffectsOfType; - private Dictionary> statusEffectLists; - + private readonly bool[] hasStatusEffectsOfType; + private readonly Dictionary> statusEffectLists; + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -232,7 +234,6 @@ namespace Barotrauma { if (character != null && character.IsOnPlayerTeam) { - return IsPlayerTeamInteractable; } else @@ -254,6 +255,12 @@ namespace Barotrauma { if (!Prefab.AllowRotatingInEditor) { return; } rotationRad = MathHelper.ToRadians(value); +#if CLIENT + if (Screen.Selected == GameMain.SubEditorScreen) + { + SetContainedItemPositions(); + } +#endif } } @@ -449,7 +456,7 @@ namespace Barotrauma } public bool IsFullCondition => MathUtils.NearlyEqual(Condition, MaxCondition); - public float MaxCondition => Prefab.Health * healthMultiplier; + public float MaxCondition => Prefab.Health * healthMultiplier * maxRepairConditionMultiplier * (1.0f + GetQualityModifier(Items.Components.Quality.StatType.Condition)); public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); private float offsetOnSelectedMultiplier = 1.0f; @@ -467,10 +474,16 @@ namespace Barotrauma public float HealthMultiplier { get => healthMultiplier; - set - { - healthMultiplier = value; - } + set { healthMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); } + } + + private float maxRepairConditionMultiplier = 1.0f; + + [Serialize(1.0f, true)] + public float MaxRepairConditionMultiplier + { + get => maxRepairConditionMultiplier; + set { maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); } } //the default value should be Prefab.Health, but because we can't use it in the attribute, @@ -541,6 +554,12 @@ namespace Barotrauma set => indestructible = value; } + public bool AllowDeconstruct + { + get; + set; + } + [Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")] public bool InvulnerableToDamage { get; set; } @@ -614,6 +633,21 @@ namespace Barotrauma get { return Prefab.UseInHealthInterface; } } + public int Quality + { + get + { + return qualityComponent?.QualityLevel ?? 0; + } + set + { + if (qualityComponent != null) + { + qualityComponent.QualityLevel = value; + } + } + } + public bool InWater { get @@ -768,6 +802,8 @@ namespace Barotrauma condition = MaxCondition; lastSentCondition = condition; + AllowDeconstruct = itemPrefab.AllowDeconstruct; + allPropertyObjects.Add(this); XElement element = itemPrefab.ConfigElement; @@ -927,6 +963,8 @@ namespace Barotrauma ownInventory = itemContainer.Inventory; } + qualityComponent = GetComponent(); + InitProjSpecific(); if (callOnItemLoaded) @@ -944,6 +982,9 @@ namespace Barotrauma if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag("logic")) { isLogic = true; } + + ApplyStatusEffects(ActionType.OnSpawn, 1.0f); + Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); } partial void InitProjSpecific(); @@ -1116,7 +1157,12 @@ namespace Barotrauma if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty(); } return components.Where(c => c is T).Cast(); } - + + public float GetQualityModifier(Quality.StatType statType) + { + return GetComponent()?.GetValue(statType) ?? 0.0f; + } + public void RemoveContained(Item contained) { ownInventory?.RemoveItem(contained); @@ -1278,7 +1324,7 @@ namespace Barotrauma } Submarine = parentInventory.Owner.Submarine; - if (body != null) body.Submarine = Submarine; + if (body != null) { body.Submarine = Submarine; } return CurrentHull; } @@ -1432,7 +1478,7 @@ namespace Barotrauma bool hasTargets = effect.TargetIdentifiers == null; targets.Clear(); - + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { foreach (Item containedItem in ContainedItems) @@ -1444,6 +1490,11 @@ namespace Barotrauma continue; } + if (effect.TargetSlot > -1) + { + if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + } + hasTargets = true; targets.Add(containedItem); } @@ -1501,8 +1552,8 @@ namespace Barotrauma { targets.Add(limb); } - - if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) targets.Add(Container); + + if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); } effect.Apply(type, deltaTime, this, targets, worldPosition); } @@ -1562,7 +1613,10 @@ namespace Barotrauma } } - aiTarget?.Update(deltaTime); + if (aiTarget != null) + { + aiTarget.Update(deltaTime); + } GameMain.Lua.hook.Call("itemThink." + prefab.Identifier, new object[] { this, deltaTime }); @@ -1682,10 +1736,18 @@ namespace Barotrauma public void UpdateTransform() { if (body == null) { return; } - Submarine prevSub = Submarine; - FindHull(); + var projectile = GetComponent(); + if (projectile?.StickTarget?.UserData is Limb limb) + { + Submarine = body.Submarine = limb.character?.Submarine; + currentHull = limb.character?.CurrentHull; + } + else + { + FindHull(); + } if (Submarine == null && prevSub != null) { @@ -1752,7 +1814,7 @@ namespace Barotrauma Vector2 drag = body.LinearVelocity * volume; - body.ApplyForce((uplift - drag) * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + body.ApplyForce((uplift - drag) * 10.0f); //apply simple angular drag body.ApplyTorque(body.AngularVelocity * volume * -0.05f); @@ -1763,6 +1825,12 @@ namespace Barotrauma { if (transformDirty) { return false; } + var projectile = GetComponent(); + if (projectile?.IgnoredBodies != null) + { + if (projectile.IgnoredBodies.Contains(f2.Body)) { return false; } + } + contact.GetWorldManifold(out Vector2 normal, out _); if (contact.FixtureA.Body == f1.Body) { normal = -normal; } float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); @@ -1822,7 +1890,8 @@ namespace Barotrauma foreach (ItemComponent component in components) { component.FlipX(relativeToSub); - } + } + SetContainedItemPositions(); } public override void FlipY(bool relativeToSub) @@ -1847,6 +1916,7 @@ namespace Barotrauma { component.FlipY(relativeToSub); } + SetContainedItemPositions(); } /// @@ -1906,17 +1976,17 @@ namespace Barotrauma return connectedComponents; } - - public static readonly Pair[] connectionPairs = new Pair[] + + public static readonly (string input, string output)[] connectionPairs = new (string input, string output)[] { - new Pair("power_in", "power_out"), - new Pair("signal_in1", "signal_out1"), - new Pair("signal_in2", "signal_out2"), - new Pair("signal_in3", "signal_out3"), - new Pair("signal_in4", "signal_out4"), - new Pair("signal_in", "signal_out"), - new Pair("signal_in1", "signal_out"), - new Pair("signal_in2", "signal_out") + ("power_in", "power_out"), + ("signal_in1", "signal_out1"), + ("signal_in2", "signal_out2"), + ("signal_in3", "signal_out3"), + ("signal_in4", "signal_out4"), + ("signal_in", "signal_out"), + ("signal_in1", "signal_out"), + ("signal_in2", "signal_out") }; private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents) where T : ItemComponent @@ -1952,20 +2022,20 @@ namespace Barotrauma recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); } - foreach (Pair connectionPair in connectionPairs) + foreach ((string input, string output) in connectionPairs) { - if (connectionPair.First == c.Name) + if (input == c.Name) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.Second); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == output); if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents); } } - else if (connectionPair.Second == c.Name) + else if (output == c.Name) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.First); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == input); if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { continue; } @@ -1975,18 +2045,27 @@ namespace Barotrauma } } - public Controller FindController() + public Controller FindController(string[] tags = null) { //try finding the controller with the simpler non-recursive method first var controllers = GetConnectedComponents(); - if (controllers.None()) { controllers = GetConnectedComponents(recursive: true); } - return controllers.Count < 2 ? controllers.FirstOrDefault() : - (controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault()); + bool needsTag = tags != null && tags.Length > 0; + if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags)))) + { + controllers = GetConnectedComponents(recursive: true); + } + if (needsTag) + { + controllers.RemoveAll(c => !c.Item.HasTag(tags)); + } + return controllers.Count < 2 ? + controllers.FirstOrDefault() : + controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault(); } - public bool TryFindController(out Controller controller) + public bool TryFindController(out Controller controller, string[] tags = null) { - controller = FindController(); + controller = FindController(tags: tags); return controller != null; } @@ -2058,8 +2137,6 @@ namespace Barotrauma } while (CoroutineManager.DeltaTime <= 0.0f); delayedSignals.Remove((signal, connection)); - - signal.source = this; connection.SendSignal(signal); yield return CoroutineStatus.Success; @@ -2313,8 +2390,8 @@ namespace Barotrauma return; //can't apply treatment to dead characters - if (character.IsDead) return; - if (!UseInHealthInterface) return; + if (character.IsDead) { return; } + if (!UseInHealthInterface) { return; } #if CLIENT if (GameMain.Client != null) @@ -2324,10 +2401,12 @@ namespace Barotrauma } #endif + float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f; + bool remove = false; foreach (ItemComponent ic in components) { - if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) continue; + if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; } bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user); ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; @@ -2336,7 +2415,19 @@ namespace Barotrauma ic.PlaySound(actionType, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction); + + if (applyOnSelfFraction > 0.0f) + { + //hacky af + ic.statusEffectLists.TryGetValue(actionType, out var effectList); + if (effectList != null) + { + effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction); + ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user); + effectList.ForEach(e => e.AfflictionMultiplier = 1.0f); + } + } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { @@ -2346,9 +2437,18 @@ namespace Barotrauma }); } - if (ic.DeleteOnUse) remove = true; + if (ic.DeleteOnUse) { remove = true; } } + if (user != null) + { + var abilityItem = new AbilityApplyTreatment(user, character, this); + user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem); + + } + + + if (remove) { Spawner?.AddToRemoveQueue(this); } } @@ -2424,6 +2524,8 @@ namespace Barotrauma parentInventory.RemoveItem(this); parentInventory = null; } + + SetContainedItemPositions(); } public void Equip(Character character) @@ -2542,6 +2644,14 @@ namespace Barotrauma { msg.Write((int)value); } + else if (value is string[] a) + { + msg.Write(a.Length); + for (int i = 0; i < a.Length; i++) + { + msg.Write(a[i] ?? ""); + } + } else { throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported"); @@ -2649,6 +2759,19 @@ namespace Barotrauma logValue = XMLExtensions.RectToString(val); if (allowEditing) { property.TrySetValue(parentObject, val); } } + else if (type == typeof(string[])) + { + int arrayLength = msg.ReadInt32(); + string[] val = new string[arrayLength]; + for (int i = 0; i < arrayLength; i++) + { + val[i] = msg.ReadString(); + } + if (allowEditing) + { + property.TrySetValue(parentObject, val); + } + } else if (typeof(Enum).IsAssignableFrom(type)) { int intVal = msg.ReadInt32(); @@ -2684,7 +2807,7 @@ namespace Barotrauma { CoroutineManager.StopCoroutines(logPropertyChangeCoroutine); } - logPropertyChangeCoroutine = CoroutineManager.InvokeAfter(() => + logPropertyChangeCoroutine = CoroutineManager.Invoke(() => { if(sender.Character != null) GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); @@ -3074,6 +3197,8 @@ namespace Barotrauma } } + connections?.Clear(); + if (parentInventory != null) { if (parentInventory is CharacterInventory characterInventory) @@ -3099,6 +3224,8 @@ namespace Barotrauma body = null; } + CurrentHull = null; + if (StaticFixtures != null) { foreach (Fixture fixture in StaticFixtures) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index d449ad8c9..cf1870762 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -9,7 +9,7 @@ namespace Barotrauma { partial class ItemInventory : Inventory { - private ItemContainer container; + private readonly ItemContainer container; public ItemContainer Container { get { return container; } @@ -47,23 +47,23 @@ namespace Barotrauma { if (ItemOwnsSelf(item)) { return false; } if (i < 0 || i >= slots.Length) { return false; } - if (!container.CanBeContained(item)) { return false; } - return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.MaxStackSize; + if (!container.CanBeContained(item, i)) { return false; } + return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.GetMaxStackSize(i); } - public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition) + public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { if (i < 0 || i >= slots.Length) { return false; } - if (!container.CanBeContained(itemPrefab)) { return false; } - return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition) && slots[i].ItemCount < container.MaxStackSize; + if (!container.CanBeContained(itemPrefab, i)) { return false; } + return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].ItemCount < container.GetMaxStackSize(i); } public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) { if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } - if (!container.CanBeContained(itemPrefab)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize), condition); + if (!container.CanBeContained(itemPrefab, i)) { return 0; } + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition); } public override bool IsFull(bool takeStacksIntoAccount = false) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.MaxStackSize)) { return false; } + if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index c7256157a..122eebb5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -17,19 +18,39 @@ namespace Barotrauma //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. public readonly float MaxCondition; //Condition of item on creation - public readonly float OutCondition; + public readonly float OutConditionMin, OutConditionMax; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; + //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this + public readonly string[] RequiredDeconstructor; + //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this + public readonly string[] RequiredOtherItem; + //text to display on the deconstructor's activate button when this output is available + public readonly string ActivateButtonText; + public readonly string InfoText; + public readonly string InfoTextOnOtherItemMissing; + public float Commonness { get; } - public DeconstructItem(XElement element) + public DeconstructItem(XElement element, string parentDebugName) { ItemIdentifier = element.GetAttributeString("identifier", "notfound"); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); - OutCondition = element.GetAttributeFloat("outcondition", 1.0f); + OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); + OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); + if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) + { + DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); + } + RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", new string[0]); + RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); + ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); + InfoText = element.GetAttributeString("infotext", string.Empty); + InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); + } } @@ -67,8 +88,10 @@ namespace Barotrauma public readonly List RequiredItems; public readonly string[] SuitableFabricatorIdentifiers; public readonly float RequiredTime; + public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly List RequiredSkills; + public int Amount { get; } public FabricationRecipe(XElement element, ItemPrefab itemPrefab) @@ -83,6 +106,7 @@ namespace Barotrauma RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); RequiredItems = new List(); + RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); foreach (XElement subElement in element.Elements()) @@ -281,7 +305,7 @@ namespace Barotrauma /// public List Triggers; - private List fabricationRecipeElements = new List(); + private readonly List fabricationRecipeElements = new List(); private readonly Dictionary treatmentSuitability = new Dictionary(); @@ -290,6 +314,8 @@ namespace Barotrauma /// public bool IsOverride; + public readonly ItemPrefab VariantOf; + public XElement ConfigElement { get; @@ -346,6 +372,9 @@ namespace Barotrauma [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] public bool HideConditionBar { get; set; } + [Serialize(false, false, description: "Hides the condition displayed in the item's tooltip.")] + public bool HideConditionInTooltip { get; set; } + //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item //if false, trigger areas define areas that can be used to highlight the item [Serialize(true, false)] @@ -513,6 +542,20 @@ namespace Barotrauma private set; } + [Serialize(0.0f, false)] + public float AddedRepairSpeedMultiplier + { + get; + private set; + } + + [Serialize(false, false)] + public bool CannotRepairFail + { + get; + private set; + } + [Serialize(null, false)] public string EquipConfirmationText { get; set; } @@ -732,6 +775,21 @@ namespace Barotrauma name = originalName; identifier = element.GetAttributeString("identifier", ""); + string variantOf = element.GetAttributeString("variantof", ""); + if (!string.IsNullOrEmpty(variantOf)) + { + ItemPrefab basePrefab = Find(null, variantOf); + if (basePrefab == null) + { + DebugConsole.ThrowError($"Failed to load the item variant \"{identifier}\" - could not find the base prefab \"{variantOf}\""); + } + else + { + VariantOf = basePrefab; + ConfigElement = element = CreateVariantXML(element, basePrefab); + } + } + string categoryStr = element.GetAttributeString("category", "Misc"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { @@ -787,6 +845,8 @@ namespace Barotrauma } } + name = GeneticMaterial.TryCreateName(this, element); + if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); @@ -863,7 +923,8 @@ namespace Barotrauma CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null) + if (subElement.Attribute("sourcerect") == null && + subElement.Attribute("sheetindex") == null) { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for item \"" + Name + "\"!"); } @@ -1031,7 +1092,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - DeconstructItems.Add(new DeconstructItem(deconstructItem)); + DeconstructItems.Add(new DeconstructItem(deconstructItem, identifier)); } RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); break; @@ -1044,7 +1105,7 @@ namespace Barotrauma var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { - DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement.ToString()})."); + DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement})."); } else { @@ -1095,11 +1156,8 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in item prefab \"" + Name + "\" - suitable treatments should be defined using item identifiers, not item names."); } - - string treatmentIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - + string treatmentIdentifier = (subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", string.Empty)).ToLowerInvariant(); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); - treatmentSuitability.Add(treatmentIdentifier, suitability); break; } @@ -1112,6 +1170,8 @@ namespace Barotrauma DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } + HideConditionInTooltip = element.GetAttributeBool("hideconditionintooltip", HideConditionBar); + //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { @@ -1313,5 +1373,99 @@ namespace Barotrauma public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); + + private XElement CreateVariantXML(XElement variantElement, ItemPrefab basePrefab) + { + XElement newElement = new XElement(variantElement.Name); + newElement.Add(basePrefab.ConfigElement.Attributes()); + newElement.Add(basePrefab.ConfigElement.Elements()); + + ReplaceElement(newElement, variantElement); + + void ReplaceElement(XElement element, XElement replacement) + { + List elementsToRemove = new List(); + foreach (XAttribute attribute in replacement.Attributes()) + { + ReplaceAttribute(element, attribute); + } + foreach (XElement replacementSubElement in replacement.Elements()) + { + int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); + System.Diagnostics.Debug.Assert(index > -1); + + int i = 0; + bool matchingElementFound = false; + foreach (XElement subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } + if (i == index) + { + if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) + { + //if the replacement is empty (no attributes or child elements) + //remove the element from the variant + elementsToRemove.Add(subElement); + } + else + { + ReplaceElement(subElement, replacementSubElement); + } + matchingElementFound = true; + break; + } + i++; + } + if (!matchingElementFound) + { + element.Add(replacementSubElement); + } + } + elementsToRemove.ForEach(e => e.Remove()); + } + + void ReplaceAttribute(XElement element, XAttribute newAttribute) + { + XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (existingAttribute == null) + { + element.Add(newAttribute); + return; + } + float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); + if (newAttribute.Value.StartsWith('*')) + { + string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); + if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value * multiplier)).ToString(); + } + } + else if (newAttribute.Value.StartsWith('+')) + { + string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); + if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value + addition)).ToString(); + } + } + else + { + existingAttribute.Value = newAttribute.Value; + } + } + + return newElement; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 905b91ff3..ad9b19866 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -36,11 +36,18 @@ namespace Barotrauma /// public bool ExcludeBroken { get; private set; } + private bool allowVariants = true; + public RelationType Type { get { return type; } } + /// + /// Index of the slot the target must be in when targeting a Contained item + /// + public int TargetSlot = -1; + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -77,13 +84,13 @@ namespace Barotrauma { if (item == null) { return false; } if (excludedIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } - return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); + return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (allowVariants && item.Prefab.VariantOf?.Identifier == id)); } public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } if (excludedIdentifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id))) { return false; } - return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id)); + return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (allowVariants && itemPrefab.VariantOf?.Identifier == id)); } public RelatedItem(string[] identifiers, string[] excludedIdentifiers) @@ -147,7 +154,9 @@ namespace Barotrauma foreach (Item contained in parentItem.ContainedItems) { + if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } if ((!ExcludeBroken || contained.Condition > 0.0f) && MatchesItem(contained)) { return true; } + if (CheckContained(contained)) { return true; } } return false; @@ -160,7 +169,9 @@ namespace Barotrauma new XAttribute("type", type.ToString()), new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), - new XAttribute("excludebroken", ExcludeBroken)); + new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("targetslot", TargetSlot), + new XAttribute("allowvariants", allowVariants)); if (excludedIdentifiers.Length > 0) { @@ -223,7 +234,8 @@ namespace Barotrauma RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { - ExcludeBroken = element.GetAttributeBool("excludebroken", true) + ExcludeBroken = element.GetAttributeBool("excludebroken", true), + allowVariants = element.GetAttributeBool("allowvariants", true) }; string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) @@ -271,6 +283,8 @@ namespace Barotrauma ri.IsOptional = element.GetAttributeBool("optional", false); ri.IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false); ri.MatchOnEmpty = element.GetAttributeBool("matchonempty", false); + ri.TargetSlot = element.GetAttributeInt("targetslot", -1); + return ri; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index b09272cc1..47525f812 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -30,55 +30,44 @@ namespace Barotrauma private bool idFreed; - public virtual bool Removed - { - get; - private set; - } + public virtual bool Removed { get; private set; } - public bool IdFreed - { - get { return idFreed; } - } + public bool IdFreed => idFreed; public readonly ushort ID; - public virtual Vector2 SimPosition + public virtual Vector2 SimPosition => Vector2.Zero; + + public virtual Vector2 Position => Vector2.Zero; + + public virtual Vector2 WorldPosition => Submarine == null ? Position : Submarine.Position + Position; + + public virtual Vector2 DrawPosition => Submarine == null ? Position : Submarine.DrawPosition + Position; + + public Submarine Submarine { get; set; } + + public AITarget AiTarget => aiTarget; + + public bool InDetectable { - get { return Vector2.Zero; } - } - - public virtual Vector2 Position - { - get { return Vector2.Zero; } - } - - public virtual Vector2 WorldPosition - { - get { return Submarine == null ? Position : Submarine.Position + Position; } - } - - public virtual Vector2 DrawPosition - { - get { return Submarine == null ? Position : Submarine.DrawPosition + Position; } - } - - public Submarine Submarine - { - get; - set; - } - - public AITarget AiTarget - { - get { return aiTarget; } - } - - public double SpawnTime - { - get { return spawnTime; } + get + { + if (aiTarget != null) + { + return aiTarget.InDetectable; + } + return false; + } + set + { + if (aiTarget != null) + { + aiTarget.InDetectable = value; + } + } } + public double SpawnTime => spawnTime; private readonly double spawnTime; public Entity(Submarine submarine, ushort id) @@ -88,7 +77,7 @@ namespace Barotrauma if (id != NullEntityID && dictionary.ContainsKey(id)) { - throw new Exception($"ID {id} is taken by {dictionary[id].ToString()}"); + throw new Exception($"ID {id} is taken by {dictionary[id]}"); } //give a unique ID diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 4e6759d60..277b55925 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -13,10 +13,8 @@ namespace Barotrauma { partial class Explosion { - private static readonly List> prevExplosions = new List>(); - public readonly Attack Attack; - + private readonly float force; private readonly float cameraShake, cameraShakeRange; @@ -35,6 +33,11 @@ namespace Barotrauma private readonly float? flashRange; private readonly string decal; private readonly float decalSize; + // used to apply friendly afflictions in an area without effects displaying + private readonly bool abilityExplosion; + private readonly bool applyToSelf; + + private readonly float itemRepairStrength; public float EmpStrength { get; set; } @@ -63,22 +66,26 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - sparks = element.GetAttributeBool("sparks", true); - shockwave = element.GetAttributeBool("shockwave", true); - flames = element.GetAttributeBool("flames", true); - underwaterBubble = element.GetAttributeBool("underwaterbubble", true); - smoke = element.GetAttributeBool("smoke", true); + abilityExplosion = element.GetAttributeBool("abilityexplosion", false); + applyToSelf = element.GetAttributeBool("applytoself", true); - playTinnitus = element.GetAttributeBool("playtinnitus", true); + bool showEffects = !abilityExplosion; + sparks = element.GetAttributeBool("sparks", showEffects); + shockwave = element.GetAttributeBool("shockwave", showEffects); + flames = element.GetAttributeBool("flames", showEffects); + underwaterBubble = element.GetAttributeBool("underwaterbubble", showEffects); + smoke = element.GetAttributeBool("smoke", showEffects); - applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); + playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); + + applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); onlyInside = element.GetAttributeBool("onlyinside", false); onlyOutside = element.GetAttributeBool("onlyoutside", false); - flash = element.GetAttributeBool("flash", true); + flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); @@ -86,15 +93,18 @@ namespace Barotrauma EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); - decal = element.GetAttributeString("decal", ""); + itemRepairStrength = element.GetAttributeFloat("itemrepairstrength", 0.0f); + + decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range); + cameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f); + cameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f); - screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f); + screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f); + } public void DisableParticles() @@ -107,19 +117,8 @@ namespace Barotrauma underwaterBubble = false; } - public List> GetRecentExplosions(float maxSecondsAgo) - { - return prevExplosions.FindAll(e => e.Third >= Timing.TotalTime - maxSecondsAgo); - } - public void Explode(Vector2 worldPosition, Entity damageSource, Character attacker = null) { - prevExplosions.Add(new Triplet(this, worldPosition, (float)Timing.TotalTime)); - if (prevExplosions.Count > 100) - { - prevExplosions.RemoveAt(0); - } - Hull hull = Hull.FindHull(worldPosition); ExplodeProjSpecific(worldPosition, hull); @@ -129,6 +128,11 @@ namespace Barotrauma } float displayRange = Attack.Range; + if (damageSource is Item sourceItem) + { + displayRange *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius); + Attack.DamageMultiplier *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage); + } Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; @@ -143,7 +147,7 @@ namespace Barotrauma if (displayRange < 0.1f) { return; } - if (Attack.GetStructureDamage(1.0f) > 0.0f || Attack.GetLevelWallDamage(1.0f) > 0.0f) + 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); } @@ -180,12 +184,29 @@ namespace Barotrauma } } - if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f)) + if (itemRepairStrength > 0.0f) + { + float displayRangeSqr = displayRange * displayRange; + foreach (Item item in Item.ItemList) + { + float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); + if (distSqr > displayRangeSqr) continue; + + float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + //repair repairable items + if (item.Repairables.Any()) + { + item.Condition += itemRepairStrength * distFactor; + } + } + } + + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f) && !abilityExplosion) { return; } - DamageCharacters(worldPosition, Attack, force, damageSource, attacker); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker, applyToSelf); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -195,9 +216,9 @@ namespace Barotrauma float dist = Vector2.Distance(item.WorldPosition, worldPosition); float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius)); - if (dist > Attack.Range) { continue; } + if (dist > displayRange) { continue; } - if (dist < Attack.Range * 0.5f && applyFireEffects && !item.FireProof && ignoreFireEffectsForTags.None(t => item.HasTag(t))) + if (dist < displayRange * 0.5f && applyFireEffects && !item.FireProof && ignoreFireEffectsForTags.None(t => item.HasTag(t))) { //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) @@ -224,7 +245,7 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float distFactor = 1.0f - dist / Attack.Range; + float distFactor = 1.0f - dist / displayRange; float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; Vector2 explosionPos = worldPosition; @@ -239,7 +260,7 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, bool applyToSelf) { if (attack.Range <= 0.0f) { return; } @@ -254,6 +275,8 @@ namespace Barotrauma { continue; } + if (c == attacker && !applyToSelf) { continue; } + if (onlyInside && c.Submarine == null) { continue; } else if (onlyOutside && c.Submarine != null) { continue; } @@ -317,7 +340,7 @@ 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); + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier); damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index ee40e20a9..47093bcf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -14,8 +14,8 @@ namespace Barotrauma partial class BackgroundSection { public Rectangle Rect; - public int Index; - public int RowIndex; + public ushort Index; + public ushort RowIndex; private Vector4 colorVector4; private Color color; @@ -39,7 +39,7 @@ namespace Barotrauma } } - public BackgroundSection(Rectangle rect, int index, int rowIndex) + public BackgroundSection(Rectangle rect, ushort index, ushort rowIndex) { Rect = rect; Index = index; @@ -53,7 +53,7 @@ namespace Barotrauma Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); } - public BackgroundSection(Rectangle rect, int index, float colorStrength, Color color, int rowIndex) + public BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex) { System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); @@ -471,9 +471,8 @@ namespace Barotrauma { aiTarget = new AITarget(this) { - MinSightRange = 2000, + MinSightRange = 1000, MaxSightRange = 5000, - MaxSoundRange = 5000, SoundRange = 0 }; } @@ -674,6 +673,9 @@ namespace Barotrauma Gap.UpdateHulls(); } + BackgroundSections?.Clear(); + submergedSections?.Clear(); + List fireSourcesToRemove = new List(FireSources); foreach (FireSource fireSource in fireSourcesToRemove) { @@ -784,7 +786,7 @@ namespace Barotrauma if (aiTarget != null) { - aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; + aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : MathHelper.Lerp(aiTarget.MinSightRange, aiTarget.MaxSightRange, Submarine.Velocity.Length() / 10); aiTarget.SoundRange -= deltaTime * 1000.0f; } @@ -932,7 +934,7 @@ namespace Barotrauma foreach (var gap in ConnectedGaps.Where(gap => gap.Open > 0)) { var distance = MathHelper.Max(Vector2.DistanceSquared(item.Position, gap.Position) / 1000, 1f); - item.body.ApplyForce((gap.LerpedFlowForce / distance) * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + item.body.ApplyForce((gap.LerpedFlowForce / distance) * deltaTime); } } @@ -1241,6 +1243,44 @@ namespace Barotrauma return "RoomName.Sub" + roomPos.ToString(); } + /// + /// Is this hull or any of the items inside it tagged as "airlock"? + /// + public bool IsTaggedAirlock() + { + if (RoomName != null && RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else + { + foreach (Item item in Item.ItemList) + { + if (item.CurrentHull != this && item.HasTag("airlock")) + { + return true; + } + } + } + return false; + } + + /// + /// Does this hull have any doors leading outside? + /// + /// Used to check if this character has access to the door leading outside + public bool LeadsOutside(Character character) + { + foreach (var gap in ConnectedGaps) + { + if (gap.ConnectedDoor == null) { continue; } + if (gap.IsRoomToRoom) { continue; } + if (!gap.ConnectedDoor.CanBeTraversed && (character == null || !gap.ConnectedDoor.HasAccess(character))) { continue; } + return true; + } + return false; + } + #region BackgroundSections private void CreateBackgroundSections() { @@ -1260,9 +1300,9 @@ namespace Barotrauma { for (int x = 0; x < xBackgroundMax; x++) { - int index = BackgroundSections.Count; + ushort index = (ushort)BackgroundSections.Count; int sector = (int)Math.Floor(index / (float)sectorWidth - xSectors * y) + y / sectorHeight * (int)Math.Ceiling(xSectors); - BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, y)); + BackgroundSections.Add(new BackgroundSection(new Rectangle(x * sectionWidth, y * -sectionHeight, sectionWidth, sectionHeight), index, (ushort)y)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5a0c3bd8c..698b5fbde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -528,6 +528,7 @@ namespace Barotrauma //create a tunnel from the lowest point in the main path to the abyss //to ensure there's a way to the abyss in all levels + Tunnel abyssTunnel = null; if (GenerationParams.CreateHoleToAbyss) { Point lowestPoint = mainPath.Nodes.First(); @@ -535,7 +536,7 @@ namespace Barotrauma { if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; } } - var abyssTunnel = new Tunnel( + abyssTunnel = new Tunnel( TunnelType.SidePath, new List() { lowestPoint, new Point(lowestPoint.X, 0) }, minWidth / 2, parentTunnel: mainPath); @@ -546,7 +547,7 @@ namespace Barotrauma for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } - var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole); + var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel); Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } @@ -559,7 +560,7 @@ namespace Barotrauma Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff)); } - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); GenerateAbyssArea(); GenerateCaves(mainPath); @@ -691,7 +692,10 @@ namespace Barotrauma } } } - GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); + + bool connectToParentTunnel = tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave; + GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel : null); + EnlargePath(tunnel.Cells, tunnel.MinWidth); foreach (var pathCell in tunnel.Cells) { @@ -791,6 +795,15 @@ namespace Barotrauma cells.AddRange(abyssIsland.Cells); } + List ruinPositions = new List(); + for (int i = 0; i < GenerationParams.RuinCount; i++) + { + Point ruinSize = new Point(5000); + ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, + limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize))); + CalculateTunnelDistanceField(ruinPositions); + } + //---------------------------------------------------------------------------------- // initialize the cells that are still left and insert them into the cell grid //---------------------------------------------------------------------------------- @@ -813,7 +826,9 @@ namespace Barotrauma //---------------------------------------------------------------------------------- // mirror if needed //---------------------------------------------------------------------------------- - + + int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server); + if (mirror) { HashSet mirroredEdges = new HashSet(); @@ -849,6 +864,21 @@ namespace Barotrauma foreach (AbyssIsland island in AbyssIslands) { island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height); + foreach (var cell in island.Cells) + { + if (!mirroredSites.Contains(cell.Site)) + { + if (cell.Site.Coord.X % GridCellSize < 1.0f && + cell.Site.Coord.X % GridCellSize >= 0.0f) { cell.Site.Coord.X += 1.0f; } + cell.Site.Coord.X = borders.Width - cell.Site.Coord.X; + mirroredSites.Add(cell.Site); + } + } + } + + for (int i = 0; i < ruinPositions.Count; i++) + { + ruinPositions[i] = new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y); } foreach (Cave cave in Caves) @@ -896,7 +926,7 @@ namespace Barotrauma startExitPosition.X = borders.Width - startExitPosition.X; endExitPosition.X = borders.Width - endExitPosition.X; - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(ruinPositions); } foreach (VoronoiCell cell in cells) @@ -913,8 +943,23 @@ namespace Barotrauma foreach (Cave cave in Caves) { if (cave.Area.Y > 0) - { - CreatePathToClosestTunnel(cave.StartPos); + { + List cavePathCells = CreatePathToClosestTunnel(cave.StartPos); + + var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type != TunnelType.Cave); + + WayPoint prevWp = mainTunnel.WayPoints.First(); + if (prevWp != null) + { + for (int i = 0; i < cavePathCells.Count; i++) + { + var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, newWaypoint, 500.0f); + prevWp = newWaypoint; + } + var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints); + ConnectWaypoints(prevWp, closestPathPoint, 500.0f); + } } List caveCells = new List(); @@ -940,9 +985,10 @@ namespace Barotrauma //---------------------------------------------------------------------------------- Ruins = new List(); - for (int i = 0; i < GenerationParams.RuinCount; i++) + for (int i = 0; i < ruinPositions.Count; i++) { - GenerateRuin(mainPath, mirror); + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i); + GenerateRuin(ruinPositions[i], mirror); } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -1004,7 +1050,6 @@ namespace Barotrauma } } - #if CLIENT List<(List cells, Cave parentCave)> cellBatches = new List<(List, Cave)> { @@ -1083,7 +1128,6 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -1101,6 +1145,11 @@ namespace Barotrauma // connect side paths and cave branches to their parents //---------------------------------------------------------------------------------- + foreach (Ruin ruin in Ruins) + { + GenerateRuinWayPoints(ruin); + } + foreach (Tunnel tunnel in Tunnels) { if (tunnel.ParentTunnel == null) { continue; } @@ -1343,18 +1392,7 @@ namespace Barotrauma if (wayPoints.Count > 1) { - wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); - } - - for (int n = 0; n < wayPoints.Count; n++) - { - if (wayPoints[n].Position != newWaypoint.Position) { continue; } - - wayPoints[n].linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(wayPoints[n]); - - break; + wayPoints[wayPoints.Count - 2].ConnectTo(newWaypoint); } } @@ -1363,19 +1401,17 @@ namespace Barotrauma //connect to the tunnel we're branching off from if (parentTunnel != null) { - var parentStart = FindClosestWayPoint(wayPoints.First(), parentTunnel); + var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel); if (parentStart != null) { - wayPoints.First().linkedTo.Add(parentStart); - parentStart.linkedTo.Add(wayPoints.First()); + wayPoints.First().ConnectTo(parentStart); } if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave) { - var parentEnd = FindClosestWayPoint(wayPoints.Last(), parentTunnel); + var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel); if (parentEnd != null) { - wayPoints.Last().linkedTo.Add(parentEnd); - parentEnd.linkedTo.Add(wayPoints.Last()); + wayPoints.Last().ConnectTo(parentEnd); } } } @@ -1385,45 +1421,58 @@ namespace Barotrauma { foreach (WayPoint wayPoint in tunnel.WayPoints) { - var closestWaypoint = FindClosestWayPoint(wayPoint, parentTunnel); + var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel); if (closestWaypoint == null) { continue; } if (Submarine.PickBody( ConvertUnits.ToSimUnits(wayPoint.WorldPosition), ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null) { - Vector2 diff = closestWaypoint.WorldPosition - wayPoint.WorldPosition; - float dist = diff.Length(); float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f; - - WayPoint prevWaypoint = wayPoint; - for (float x = step; x < dist - step; x += step) - { - var newWaypoint = new WayPoint(wayPoint.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null) - { - Tunnel = tunnel - }; - prevWaypoint.linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(prevWaypoint); - prevWaypoint = newWaypoint; - } - prevWaypoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(prevWaypoint); + ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel); } } } - private static WayPoint FindClosestWayPoint(WayPoint wayPoint, Tunnel otherTunnel) + private List ConnectWaypoints(WayPoint wp1, WayPoint wp2, float interval) + { + List newWaypoints = new List(); + + Vector2 diff = wp2.WorldPosition - wp1.WorldPosition; + float dist = diff.Length(); + + WayPoint prevWaypoint = wp1; + for (float x = interval; x < dist - interval; x += interval) + { + var newWaypoint = new WayPoint(wp1.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null); + prevWaypoint.ConnectTo(newWaypoint); + prevWaypoint = newWaypoint; + newWaypoints.Add(newWaypoint); + } + prevWaypoint.ConnectTo(wp2); + + return newWaypoints; + } + + private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel) + { + return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints); + } + + private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable waypoints, Func filter = null) { float closestDist = float.PositiveInfinity; WayPoint closestWayPoint = null; - foreach (WayPoint otherWayPoint in otherTunnel.WayPoints) + foreach (WayPoint otherWayPoint in waypoints) { - float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, wayPoint.WorldPosition); + float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition); if (dist < closestDist) { + if (filter != null) + { + if (!filter(otherWayPoint)) { continue; } + } closestDist = dist; closestWayPoint = otherWayPoint; - } } return closestWayPoint; @@ -1706,7 +1755,7 @@ namespace Barotrauma GenerateCave(caveParams, parentTunnel, cavePos, caveSize); - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(null); } } @@ -1788,84 +1837,172 @@ namespace Barotrauma } } - private void GenerateRuin(Tunnel mainPath, bool mirror) + private void GenerateRuin(Point ruinPos, bool mirror) { - var ruinGenerationParams = RuinGenerationParams.GetRandom(); + var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server); - Point ruinSize = new Point( - Rand.Range(ruinGenerationParams.SizeMin.X, ruinGenerationParams.SizeMax.X, Rand.RandSync.Server), - Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server)); - int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2; - - Point ruinPos = FindPosAwayFromMainPath((ruinRadius + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, - limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)); - - VoronoiCell closestPathCell = null; - double closestDist = 0.0f; - foreach (VoronoiCell pathCell in mainPath.Cells) + LocationType locationType = StartLocation?.Type; + if (locationType == null) { - double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y); - if (closestPathCell == null || dist < closestDist) + locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + if (ruinGenerationParams.AllowedLocationTypes.Any()) { - closestPathCell = pathCell; - closestDist = dist; + locationType = LocationType.List.Where(lt => + ruinGenerationParams.AllowedLocationTypes.Any(allowedType => + allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); } } - - var ruin = new Ruin(closestPathCell, cells, ruinGenerationParams, new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize), mirror); + + var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror); Ruins.Add(ruin); - - ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance)); - // TODO: autogenerate waypoints inside the ruins and connect them to the main path in multiple places. - // We need the waypoints for the AI navigation and we could use them for spawning the creatures too. - int waypointCount = 0; - foreach (WayPoint wp in WayPoint.WayPointList) + var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4); + + foreach (VoronoiCell cell in tooClose) { - if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; } - if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition))) + if (cell.CellType == CellType.Empty) { continue; } + if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; } + foreach (GraphEdge e in cell.Edges) { - PositionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin, ruin: ruin)); - waypointCount++; - } - } - - //not enough waypoints inside ruins -> create some spawn positions manually - for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++) - { - PositionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin, ruin: ruin)); - } - - foreach (RuinShape ruinShape in ruin.RuinShapes) - { - var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height) * 4); - - foreach (VoronoiCell cell in tooClose) - { - if (cell.CellType == CellType.Empty) { continue; } - if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; } - foreach (GraphEdge e in cell.Edges) + if (ruin.Area.Contains(e.Point1) || ruin.Area.Contains(e.Point2) || + MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, ruin.Area, out _)) { - Rectangle rect = ruinShape.Rect; - rect.Y += rect.Height; - if (ruinShape.Rect.Contains(e.Point1) || ruinShape.Rect.Contains(e.Point2) || - MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, rect, out _)) + cell.CellType = CellType.Removed; + for (int x = 0; x < cellGrid.GetLength(0); x++) { - cell.CellType = CellType.Removed; - for (int x = 0; x < cellGrid.GetLength(0); x++) + for (int y = 0; y < cellGrid.GetLength(1); y++) { - for (int y = 0; y < cellGrid.GetLength(1); y++) - { - cellGrid[x, y].Remove(cell); - } + cellGrid[x, y].Remove(cell); } - cells.Remove(cell); - break; } + cells.Remove(cell); + break; } } } - CreatePathToClosestTunnel(ruinPos); + ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center); + } + + private void GenerateRuinWayPoints(Ruin ruin) + { + var tooClose = GetTooCloseCells(ruin.Area.Center.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 6); + + List wayPoints = new List(); + float outSideWaypointInterval = 500.0f; + WayPoint[,] cornerWaypoint = new WayPoint[2, 2]; + Rectangle waypointArea = ruin.Area; + waypointArea.Inflate(100, 100); + + //generate waypoints around the ruin + for (int i = 0; i < 2; i++) + { + for (float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval) + { + var wayPoint = new WayPoint(new Vector2(x, waypointArea.Y + waypointArea.Height * i), SpawnType.Path, null) + { + Ruin = ruin + }; + wayPoints.Add(wayPoint); + if (x == waypointArea.X + outSideWaypointInterval) + { + cornerWaypoint[i, 0] = wayPoint; + } + else + { + wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]); + } + } + cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1]; + } + + for (int i = 0; i < 2; i++) + { + WayPoint wayPoint = null; + for (float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval) + { + wayPoint = new WayPoint(new Vector2(waypointArea.X + waypointArea.Width * i, y), SpawnType.Path, null) + { + Ruin = ruin + }; + wayPoints.Add(wayPoint); + if (y == waypointArea.Y) + { + wayPoint.ConnectTo(cornerWaypoint[0, i]); + } + else + { + wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]); + } + } + wayPoint.ConnectTo(cornerWaypoint[1, i]); + } + + //remove waypoints that are inside walls + for (int i = wayPoints.Count - 1; i >= 0; i--) + { + WayPoint wp = wayPoints[i]; + var overlappingCell = tooClose.Find(c => c.CellType != CellType.Removed && c.IsPointInside(wp.WorldPosition)); + if (overlappingCell == null) { continue; } + if (wp.linkedTo.Count > 1) + { + WayPoint linked1 = wp.linkedTo[0] as WayPoint; + WayPoint linked2 = wp.linkedTo[1] as WayPoint; + linked1.ConnectTo(linked2); + } + wp.Remove(); + wayPoints.RemoveAt(i); + } + + //connect ruin entrances to the outside waypoints + foreach (Gap g in Gap.GapList) + { + if (g.Submarine != ruin.Submarine || g.IsRoomToRoom || g.linkedTo.Count == 0) { continue; } + var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g); + if (gapWaypoint == null) { continue; } + + //place another waypoint in front of the entrance + Vector2 entranceDir = Vector2.Zero; + if (g.IsHorizontal) + { + entranceDir = Vector2.UnitX * 2 * Math.Sign(g.WorldPosition.X - g.linkedTo[0].WorldPosition.X); + } + else + { + entranceDir = Vector2.UnitY * 2 * Math.Sign(g.WorldPosition.Y - g.linkedTo[0].WorldPosition.Y); + } + var entranceWayPoint = new WayPoint(g.WorldPosition + entranceDir * 64.0f, SpawnType.Path, null) + { + Ruin = ruin + }; + entranceWayPoint.ConnectTo(gapWaypoint); + var closestWp = FindClosestWayPoint(entranceWayPoint.WorldPosition, wayPoints, (wp) => + { + return Submarine.PickBody( + ConvertUnits.ToSimUnits(wp.WorldPosition), + ConvertUnits.ToSimUnits(entranceWayPoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null; + }); + if (closestWp == null) { continue; } + ConnectWaypoints(entranceWayPoint, closestWp, outSideWaypointInterval); + } + + //create a waypoint path from the ruin to the closest tunnel + WayPoint prevWp = FindClosestWayPoint(ruin.PathCells.First().Center, wayPoints, (wp) => + { + return Submarine.PickBody( + ConvertUnits.ToSimUnits(wp.WorldPosition), + ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null; + }); + if (prevWp != null) + { + for (int i = 0; i < ruin.PathCells.Count; i++) + { + var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval); + prevWp = newWaypoint; + } + var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, Tunnels.SelectMany(t => t.WayPoints)); + ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval); + } } private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null) @@ -1891,8 +2028,9 @@ namespace Barotrauma } } - private void CalculateTunnelDistanceField(int density) + private void CalculateTunnelDistanceField(List ruinPositions) { + int density = 1000; distanceField = new List<(Point point, double distance)>(); if (Mirrored) @@ -1927,6 +2065,23 @@ namespace Barotrauma shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); } } + if (ruinPositions != null) + { + int ruinSize = 10000; + foreach (Point ruinPos in ruinPositions) + { + double xDiff = Math.Abs(point.X - ruinPos.X); + double yDiff = Math.Abs(point.Y - ruinPos.Y); + if (xDiff < ruinSize || yDiff < ruinSize) + { + shortestDistSqr = 0.0f; + } + else + { + shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr); + } + } + } shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); @@ -2564,10 +2719,18 @@ namespace Barotrauma if (PositionsOfInterest.Any(p => p.PositionType == PositionType.Cave)) { positionType = PositionType.Cave; + if (allValidLocations.Any(l => l.Edge.NextToCave)) + { + allValidLocations.RemoveAll(l => !l.Edge.NextToCave); + } } else if (PositionsOfInterest.Any(p => p.PositionType == PositionType.SidePath)) { positionType = PositionType.SidePath; + if (allValidLocations.Any(l => l.Edge.NextToSidePath)) + { + allValidLocations.RemoveAll(l => !l.Edge.NextToSidePath); + } } var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server); @@ -2946,7 +3109,7 @@ namespace Barotrauma return closestCell; } - private void CreatePathToClosestTunnel(Point pos) + private List CreatePathToClosestTunnel(Point pos) { VoronoiCell closestPathCell = null; double closestDist = 0.0f; @@ -2966,6 +3129,7 @@ namespace Barotrauma //cast a ray from the closest path cell towards the position and remove the cells it hits List validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed); + List pathCells = new List() { closestPathCell }; foreach (VoronoiCell cell in validCells) { foreach (GraphEdge e in cell.Edges) @@ -2980,6 +3144,7 @@ namespace Barotrauma cellGrid[x, y].Remove(cell); } } + pathCells.Add(cell); cells.Remove(cell); //go through the edges of this cell and find the ones that are next to a removed cell @@ -3012,12 +3177,12 @@ namespace Barotrauma } } } - - break; - } } + + pathCells.Sort((c1, c2) => { return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); }); + return pathCells; } public string GetWreckIDTag(string originalTag, Submarine wreck) @@ -3049,10 +3214,8 @@ namespace Barotrauma var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && - wp.WorldPosition.X < EndExitPosition.X && !IsCloseToStart(wp.WorldPosition, minDistance) && - !IsCloseToEnd(wp.WorldPosition, minDistance) - ).ToList(); + !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); var subDoc = SubmarineInfo.OpenFile(contentFile.Path); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); @@ -3137,7 +3300,7 @@ namespace Barotrauma } tempSW.Stop(); Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); - sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false); + sub.SetPosition(spawnPoint); wreckPositions.Add(sub, positions); blockedRects.Add(sub, rects); return sub; @@ -3468,7 +3631,7 @@ namespace Barotrauma { locationType = LocationType.List.Where(lt => outpostGenerationParams.AllowedLocationTypes.Any(allowedType => - allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(); + allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); } } @@ -3649,9 +3812,10 @@ namespace Barotrauma } if (LevelData.IsBeaconActive) { - if (reactorContainer != null && reactorContainer.Inventory.IsEmpty()) + if (reactorContainer != null && reactorContainer.Inventory.IsEmpty() && + reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault())) { - ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItems[0].Identifiers[0]]; + ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()]; Spawner.AddToSpawnQueue( fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); @@ -3826,7 +3990,7 @@ namespace Barotrauma bool TryGetExtraSpawnPoint(out Vector2 point) { point = Vector2.Zero; - var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(); + var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(Rand.RandSync.Unsynced); if (hull != null) { point = hull.WorldPosition; @@ -3891,12 +4055,39 @@ namespace Barotrauma LevelObjectManager = null; } + AbyssIslands?.Clear(); + AbyssResources?.Clear(); + Caves?.Clear(); + Tunnels?.Clear(); + PathPoints?.Clear(); + PositionsOfInterest?.Clear(); + + wreckPositions?.Clear(); + Wrecks?.Clear(); + + BeaconStation = null; + beaconSonar = null; + StartOutpost = null; + EndOutpost = null; + + blockedRects?.Clear(); + + EntitiesBeforeGenerate?.Clear(); + EqualityCheckValues?.Clear(); + if (Ruins != null) { Ruins.Clear(); Ruins = null; } + bottomPositions?.Clear(); + BottomBarrier = null; + TopBarrier = null; + SeaFloor = null; + + distanceField = null; + if (ExtraWalls != null) { foreach (LevelWall w in ExtraWalls) { w.Dispose(); } @@ -3908,7 +4099,9 @@ namespace Barotrauma UnsyncedExtraWalls = null; } + tempCells?.Clear(); cells = null; + cellGrid = null; if (bodies != null) { @@ -3916,6 +4109,9 @@ namespace Barotrauma bodies = null; } + StartLocation = null; + EndLocation = null; + Loaded = null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index e4288f792..1d6643992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -84,25 +84,42 @@ namespace Barotrauma objectGrid = new List[ level.Size.X / GridSize, (level.Size.Y - level.BottomPos) / GridSize]; - + List availableSpawnPositions = new List(); var levelCells = level.GetAllCells(); - availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall)); + availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall)); availableSpawnPositions.AddRange(GetAvailableSpawnPositions(level.SeaFloor.Cells, LevelObjectPrefab.SpawnPosType.SeaFloor)); - - foreach (RuinGeneration.Ruin ruin in level.Ruins) + + foreach (Structure structure in Structure.WallList) { - foreach (var ruinShape in ruin.RuinShapes) + if (!structure.HasBody || structure.HiddenInGame) { continue; } + if (level.Ruins.Any(r => r.Submarine == structure.Submarine)) { - foreach (var wall in ruinShape.Walls) + if (structure.IsHorizontal) { + bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; + bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; + if (topHull && bottomHull ) { continue; } + availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(wall.A, wall.B), - (wall.A + wall.B) / 2.0f - ruinShape.Center, + new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), + bottomHull ? Vector2.UnitY : -Vector2.UnitY, LevelObjectPrefab.SpawnPosType.RuinWall, - ruinShape.GetLineAlignment(wall))); + bottomHull ? Alignment.Bottom : Alignment.Top)); } - } + else + { + bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; + bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; + if (rightHull && leftHull) { continue; } + + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), + leftHull ? Vector2.UnitX : -Vector2.UnitX, + LevelObjectPrefab.SpawnPosType.RuinWall, + leftHull ? Alignment.Left : Alignment.Right)); + } + } } foreach (var posOfInterest in level.PositionsOfInterest) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 6259d240d..71a30f339 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial class LevelTrigger { [Flags] - enum TriggererType + public enum TriggererType { None = 0, Human = 1, @@ -258,13 +258,17 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type."); } - UpdateCollisionCategories(); + if (PhysicsBody != null) + { + PhysicsBody.CollidesWith = GetCollisionCategories(triggeredBy); + } + TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); var tagsArray = element.GetAttributeStringArray("tags", new string[0]); foreach (string tag in tagsArray) { - tags.Add(tag.ToLower()); + tags.Add(tag.ToLowerInvariant()); } if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) @@ -272,30 +276,21 @@ namespace Barotrauma var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]); foreach (string tag in otherTagsArray) { - allowedOtherTriggerTags.Add(tag.ToLower()); + allowedOtherTriggerTags.Add(tag.ToLowerInvariant()); } } + string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}"; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in "+ parentDebugName)); + LoadStatusEffect(statusEffects, subElement, debugName); break; case "attack": case "damage": - var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName); - if (!triggerOnce) - { - var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); - attack.Afflictions.Clear(); - foreach (Affliction affliction in multipliedAfflictions) - { - attack.Afflictions.Add(affliction, null); - } - } - attacks.Add(attack); + LoadAttack(subElement, debugName, triggerOnce, attacks); break; } } @@ -304,16 +299,13 @@ namespace Barotrauma randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval); } - private void UpdateCollisionCategories() + public static Category GetCollisionCategories(TriggererType triggeredBy) { - if (PhysicsBody == null) return; - var collidesWith = Physics.CollisionNone; if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; } if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; } if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; } - - PhysicsBody.CollidesWith = collidesWith; + return collidesWith; } private void CalculateDirectionalForce() @@ -326,33 +318,31 @@ namespace Barotrauma -sa * unrotatedForce.X + ca * unrotatedForce.Y); } - private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact) + public static void LoadStatusEffect(List statusEffects, XElement element, string parentDebugName) + { + statusEffects.Add(StatusEffect.Load(element, parentDebugName)); + } + + public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List attacks) + { + var attack = new Attack(element, parentDebugName); + if (!triggerOnce) + { + var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); + attack.Afflictions.Clear(); + foreach (Affliction affliction in multipliedAfflictions) + { + attack.Afflictions.Add(affliction, null); + } + } + attacks.Add(attack); + } + + private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); - if (entity == null) return false; - - if (entity is Character character) - { - if (character.CurrentHull != null) return false; - if (character.IsHuman) - { - if (!triggeredBy.HasFlag(TriggererType.Human)) return false; - } - else - { - if (!triggeredBy.HasFlag(TriggererType.Creature)) return false; - } - } - else if (entity is Item item) - { - if (item.CurrentHull != null) return false; - if (!triggeredBy.HasFlag(TriggererType.Item)) return false; - } - else if (entity is Submarine) - { - if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false; - } - + if (entity == null) { return false; } + if (!IsTriggeredByEntity(entity, triggeredBy, mustBeOutside: true)) { return false; } if (!triggerers.Contains(entity)) { if (!IsTriggered) @@ -365,6 +355,34 @@ namespace Barotrauma return true; } + public static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside = false, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default) + { + if (entity is Character character) + { + if (mustBeOutside && character.CurrentHull != null) { return false; } + if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; } + if (character.IsHuman) + { + if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; } + } + else + { + if (!triggeredBy.HasFlag(TriggererType.Creature)) { return false; } + } + } + else if (entity is Item item) + { + if (mustBeOutside && item.CurrentHull != null) { return false; } + if (mustBeOnSpecificSub.mustBe && item.Submarine != mustBeOnSpecificSub.sub) { return false; } + if (!triggeredBy.HasFlag(TriggererType.Item)) { return false; } + } + else if (entity is Submarine) + { + if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; } + } + return true; + } + private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); @@ -379,10 +397,21 @@ namespace Barotrauma return; } + if (CheckContactsForOtherFixtures(PhysicsBody, fixtureB, entity)) { return; } + + if (triggerers.Contains(entity)) + { + TriggererPosition.Remove(entity); + triggerers.Remove(entity); + } + } + + public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, 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 PhysicsBody.FarseerBody.FixtureList) + foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) { ContactEdge contactEdge = fixture.Body.ContactList; while (contactEdge != null) @@ -393,30 +422,24 @@ namespace Barotrauma { if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? + var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ? contactEdge.Contact.FixtureB : contactEdge.Contact.FixtureA); - if (otherEntity == entity) { return; } + if (otherEntity == separatingEntity) { return true; } } } contactEdge = contactEdge.Next; } } - - if (triggerers.Contains(entity)) - { - TriggererPosition.Remove(entity); - triggerers.Remove(entity); - } + return false; } - private Entity GetEntity(Fixture fixture) + public static Entity GetEntity(Fixture fixture) { if (fixture.Body == null || fixture.Body.UserData == null) { return null; } if (fixture.Body.UserData is Entity entity) { return entity; } if (fixture.Body.UserData is Limb limb) { return limb.character; } if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; } - return null; } @@ -452,15 +475,7 @@ namespace Barotrauma triggerers.RemoveWhere(t => t.Removed); - if (PhysicsBody != null) - { - //failsafe to ensure triggerers get removed when they're far from the trigger - float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f); - triggerers.RemoveWhere(t => - { - return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent; - }); - } + RemoveDistantTriggerers(PhysicsBody, triggerers, WorldPosition); bool isNotClient = true; #if CLIENT @@ -525,57 +540,15 @@ namespace Barotrauma foreach (Entity triggerer in triggerers) { - foreach (StatusEffect effect in statusEffects) - { - if (effect.type == ActionType.OnBroken) { continue; } - Vector2? position = null; - if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; } - if (triggerer is Character character) - { - effect.Apply(effect.type, deltaTime, triggerer, character, position); - if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) - { - foreach (Item item in character.Inventory.AllItemsMod) - { - if (item.ContainedItems == null) { continue; } - foreach (Item containedItem in item.ContainedItems) - { - effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); - } - } - } - } - else if (triggerer is Item item) - { - effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); - } - if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || - effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); - effect.Apply(effect.type, deltaTime, triggerer, targets); - } - } + ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets); if (triggerer is IDamageable damageable) { - foreach (Attack attack in attacks) - { - attack.DoDamage(null, damageable, WorldPosition, deltaTime, false); - } + ApplyAttacks(attacks, damageable, worldPosition, deltaTime); } else if (triggerer is Submarine submarine) { - foreach (Attack attack in attacks) - { - float structureDamage = attack.GetStructureDamage(deltaTime); - if (structureDamage > 0.0f) - { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); - } - } - + ApplyAttacks(attacks, worldPosition, deltaTime); if (!string.IsNullOrWhiteSpace(InfectIdentifier)) { submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); @@ -586,16 +559,16 @@ namespace Barotrauma { if (triggerer is Character character) { - ApplyForce(character.AnimController.Collider, deltaTime); + ApplyForce(character.AnimController.Collider); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } - ApplyForce(limb.body, deltaTime); + ApplyForce(limb.body); } } else if (triggerer is Submarine submarine) { - ApplyForce(submarine.SubBody.Body, deltaTime); + ApplyForce(submarine.SubBody.Body); } } @@ -606,12 +579,84 @@ namespace Barotrauma } } - private void ApplyForce(PhysicsBody body, float deltaTime) + public static void RemoveDistantTriggerers(PhysicsBody physicsBody, HashSet triggerers, Vector2 calculateDistanceTo) + { + //failsafe to ensure triggerers get removed when they're far from the trigger + if (physicsBody == null) { return; } + float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); + triggerers.RemoveWhere(t => + { + return Vector2.Distance(t.WorldPosition, calculateDistanceTo) > maxExtent; + }); + } + + public static void ApplyStatusEffects(List statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List targets) + { + foreach (StatusEffect effect in statusEffects) + { + if (effect.type == ActionType.OnBroken) { return; } + Vector2? position = null; + if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; } + if (triggerer is Character character) + { + effect.Apply(effect.type, deltaTime, triggerer, character, position); + if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) + { + foreach (Item item in character.Inventory.AllItemsMod) + { + if (item.ContainedItems == null) { continue; } + foreach (Item containedItem in item.ContainedItems) + { + effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); + } + } + } + } + else if (triggerer is Item item) + { + effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); + } + if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.Apply(effect.type, deltaTime, triggerer, targets); + } + } + } + + /// + /// Applies attacks to a damageable. + /// + public static void ApplyAttacks(List attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime) + { + foreach (Attack attack in attacks) + { + attack.DoDamage(null, damageable, worldPosition, deltaTime, false); + } + } + + /// + /// Applies attacks to structures. + /// + public static void ApplyAttacks(List attacks, Vector2 worldPosition, float deltaTime) + { + foreach (Attack attack in attacks) + { + float structureDamage = attack.GetStructureDamage(deltaTime); + if (structureDamage > 0.0f) + { + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); + } + } + } + + private void ApplyForce(PhysicsBody body) { float distFactor = 1.0f; if (ForceFalloff) { - distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius; + distFactor = GetDistanceFactor(body, PhysicsBody, ColliderRadius); if (distFactor < 0.0f) return; } @@ -621,19 +666,19 @@ namespace Barotrauma if (ForceVelocityLimit < 1000.0f) body.ApplyForce(Force * currentForceFluctuation * distFactor, ForceVelocityLimit); else - body.ApplyForce(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + body.ApplyForce(Force * currentForceFluctuation * distFactor); break; case TriggerForceMode.Acceleration: if (ForceVelocityLimit < 1000.0f) body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, ForceVelocityLimit); else - body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor); break; case TriggerForceMode.Impulse: if (ForceVelocityLimit < 1000.0f) body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: ForceVelocityLimit); else - body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor); break; case TriggerForceMode.LimitVelocity: float maxVel = ForceVelocityLimit * currentForceFluctuation * distFactor; @@ -648,6 +693,11 @@ namespace Barotrauma } } + public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius) + { + return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius; + } + public Vector2 GetWaterFlowVelocity(Vector2 viewPosition) { Vector2 baseVel = GetWaterFlowVelocity(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs deleted file mode 100644 index 1537134e1..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.RuinGeneration -{ - /// - /// nodes of a binary tree used for generating underwater "dungeons" - /// - class BTRoom : RuinShape - { - private BTRoom[] subRooms; - - public BTRoom Parent - { - get; - private set; - } - - public Corridor Corridor - { - get; - set; - } - - public BTRoom[] SubRooms - { - get { return subRooms; } - } - - public BTRoom Adjacent - { - get; - private set; - } - - public BTRoom(Rectangle rect) - { - this.rect = rect; - } - - public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200) - { - bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability; - if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight) - { - minDivRatio = 0.5f; - } - else if (rect.Width * minDivRatio < minWidth) - { - verticalSplit = false; - } - else if (rect.Height * minDivRatio < minHeight) - { - verticalSplit = true; - } - - subRooms = new BTRoom[2]; - if (verticalSplit) - { - SplitVertical(minDivRatio); - } - else - { - SplitHorizontal(minDivRatio); - } - - subRooms[0].Parent = this; - subRooms[1].Parent = this; - - subRooms[0].Adjacent = subRooms[1]; - subRooms[1].Adjacent = subRooms[0]; - } - - private void SplitHorizontal(float minDivRatio) - { - float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server); - subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, rect.Width, (int)(rect.Height * div))); - subRooms[1] = new BTRoom(new Rectangle(rect.X, rect.Y + subRooms[0].rect.Height, rect.Width, rect.Height - subRooms[0].rect.Height)); - - } - - private void SplitVertical(float minDivRatio) - { - float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server); - subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, (int)(rect.Width * div), rect.Height)); - subRooms[1] = new BTRoom(new Rectangle(rect.X + subRooms[0].rect.Width, rect.Y, rect.Width - subRooms[0].rect.Width, rect.Height)); - } - - public override void CreateWalls() - { - Walls = new List - { - new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)), - new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)), - new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)), - new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom)) - }; - } - - public void Scale(Vector2 scale) - { - rect.Inflate((scale.X - 1.0f) * 0.5f * rect.Width, (scale.Y - 1.0f) * 0.5f * rect.Height); - } - - public List GetLeaves() - { - return GetLeaves(new List()); - } - - private List GetLeaves(List leaves) - { - if (subRooms == null) - { - leaves.Add(this); - } - else - { - subRooms[0].GetLeaves(leaves); - subRooms[1].GetLeaves(leaves); - } - - return leaves; - } - - public void GenerateCorridors(int minWidth, int maxWidth, List corridors) - { - if (Adjacent != null && Corridor == null) - { - Corridor = new Corridor(this, Rand.Range(minWidth, maxWidth, Rand.RandSync.Server), corridors); - } - - if (subRooms != null) - { - subRooms[0].GenerateCorridors(minWidth, maxWidth, corridors); - subRooms[1].GenerateCorridors(minWidth, maxWidth, corridors); - } - } - - public static void CalculateDistancesFromEntrance(BTRoom entrance, List rooms, List corridors) - { - entrance.CalculateDistanceFromEntrance(0, rooms, new List(corridors)); - } - - private void CalculateDistanceFromEntrance(int currentDist, List rooms, List corridors) - { - DistanceFromEntrance = DistanceFromEntrance == 0 ? currentDist : Math.Min(currentDist, DistanceFromEntrance); - - currentDist++; - - var roomRect = Rect; - roomRect.Inflate(5, 5); - foreach (var corridor in corridors) - { - var corridorRect = corridor.Rect; - corridorRect.Inflate(5, 5); - if (!corridorRect.Intersects(roomRect)) continue; - - corridor.DistanceFromEntrance = corridor.DistanceFromEntrance == 0 ? - DistanceFromEntrance + 1 : - Math.Min(corridor.DistanceFromEntrance, DistanceFromEntrance + 1); - - - List connectedRooms = new List(); - foreach (var otherRoom in rooms) - { - if (otherRoom == this) continue; - if (otherRoom.DistanceFromEntrance > 0 && otherRoom.DistanceFromEntrance < currentDist) continue; - - var otherRoomRect = otherRoom.Rect; - otherRoomRect.Inflate(5, 5); - if (corridorRect.Intersects(otherRoomRect)) { connectedRooms.Add(otherRoom); } - } - - connectedRooms.Sort((r1, r2) => - { - return - (Math.Abs(r1.Rect.Center.X - Rect.Center.X) + Math.Abs(r1.Rect.Center.Y - Rect.Center.Y)) - - (Math.Abs(r2.Rect.Center.X - Rect.Center.X) + Math.Abs(r2.Rect.Center.Y - Rect.Center.Y)); - }); - - for (int i = 0; i < connectedRooms.Count; i++) - { - connectedRooms[i].CalculateDistanceFromEntrance(currentDist + 1 + i, rooms, corridors); - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs deleted file mode 100644 index 7380256ef..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs +++ /dev/null @@ -1,210 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; - -namespace Barotrauma.RuinGeneration -{ - - class Corridor : RuinShape - { - private readonly bool isHorizontal; - - public bool IsHorizontal - { - get { return isHorizontal; } - } - - public BTRoom[] ConnectedRooms - { - get; - private set; - } - - public Corridor(Rectangle rect) - { - this.rect = rect; - - isHorizontal = rect.Width > rect.Height; - } - - public Corridor(BTRoom room, int width, List corridors) - { - System.Diagnostics.Debug.Assert(room.Adjacent != null); - - ConnectedRooms = new BTRoom[2]; - ConnectedRooms[0] = room; - ConnectedRooms[1] = room.Adjacent; - - Rectangle room1, room2; - - room1 = room.Rect; - room2 = room.Adjacent.Rect; - - isHorizontal = (room1.Right <= room2.X || room2.Right <= room1.X); - - //use the leaves as starting points for the corridor - if (room.SubRooms != null) - { - var leaves1 = room.GetLeaves(); - var leaves2 = room.Adjacent.GetLeaves(); - - var suitableLeaves = GetSuitableLeafRooms(leaves1, leaves2, width, isHorizontal); - if (suitableLeaves == null || suitableLeaves.Length < 2) - { - // No suitable leaves found due to intersections - //DebugConsole.ThrowError("Error while generating ruins. Could not find a suitable position for a corridor. The width of the corridors may be too large compared to the sizes of the rooms."); - return; - } - else - { - ConnectedRooms[0] = suitableLeaves[0]; - ConnectedRooms[1] = suitableLeaves[1]; - } - } - else - { - rect = CalculateRectangle(room1, room2, width, isHorizontal); - if (rect.Width <= 0 || rect.Height <= 0) - { - DebugConsole.ThrowError("Error while generating ruins. Attempted to create a corridor with a width or height of <= 0"); - return; - } - } - - room.Corridor = this; - room.Adjacent.Corridor = this; - - for (int i = corridors.Count - 1; i >= 0; i--) - { - var corridor = corridors[i]; - - if (corridor.rect.Intersects(this.rect)) - { - if (isHorizontal && corridor.isHorizontal) - { - if (this.rect.Width < corridor.rect.Width) - return; - else - corridors.RemoveAt(i); - } - else if (!isHorizontal && !corridor.isHorizontal) - { - if (this.rect.Height < corridor.rect.Height) - return; - else - corridors.RemoveAt(i); - } - } - } - - corridors.Add(this); - } - - public override void CreateWalls() - { - Walls = new List(); - if (IsHorizontal) - { - Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y))); - Walls.Add(new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom))); - } - else - { - Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom))); - Walls.Add(new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom))); - } - } - - /// - /// Find two rooms which have two face-two-face walls that we can place a corridor in between - /// - /// - private BTRoom[] GetSuitableLeafRooms(List leaves1, List leaves2, int width, bool isHorizontal) - { - int iOffset = Rand.Int(leaves1.Count, Rand.RandSync.Server); - int jOffset = Rand.Int(leaves2.Count, Rand.RandSync.Server); - - for (int iCount = 0; iCount < leaves1.Count; iCount++) - { - int i = (iCount + iOffset) % leaves1.Count; - - for (int jCount = 0; jCount < leaves2.Count; jCount++) - { - int j = (jCount + jOffset) % leaves2.Count; - - if (isHorizontal) - { - if (leaves1[i].Rect.Y > leaves2[j].Rect.Bottom - width) continue; - if (leaves1[i].Rect.Bottom < leaves2[j].Rect.Y + width) continue; - } - else - { - if (leaves1[i].Rect.X > leaves2[j].Rect.Right - width) continue; - if (leaves1[i].Rect.Right < leaves2[j].Rect.X + width) continue; - } - - // Check if the given corridor rect would intersect over a third room - if (CheckForIntersection(leaves1[i], leaves2[j], leaves1, leaves2, width, isHorizontal)) continue; - - return new BTRoom[] { leaves1[i], leaves2[j] }; - } - } - - return null; - } - - private bool CheckForIntersection(BTRoom potential1, BTRoom potential2, List leaves1, List leaves2, int width, bool isHorizontal) - { - Rectangle potentialCorridorRectangle = CalculateRectangle(potential1.Rect, potential2.Rect, width, isHorizontal); - - if (potentialCorridorRectangle.Width <= 0 || potentialCorridorRectangle.Height <= 0) return true; // Invalid rectangle - - for (int i = 0; i < leaves1.Count; i++) - { - if (leaves1[i] == potential1) continue; - if (potentialCorridorRectangle.Intersects(leaves1[i].Rect)) return true; - } - - for (int i = 0; i < leaves2.Count; i++) - { - if (leaves2[i] == potential2) continue; - if (potentialCorridorRectangle.Intersects(leaves2[i].Rect)) return true; - } - - rect = potentialCorridorRectangle; // Save the rectangle that passes the test - return false; - } - - private Rectangle CalculateRectangle(Rectangle rect1, Rectangle rect2, int width, bool isHorizontal) - { - if (isHorizontal) - { - int left = Math.Min(rect1.Right, rect2.Right); - int right = Math.Max(rect1.X, rect2.X); - - int top = Math.Max(rect1.Y, rect2.Y); - //int bottom = Math.Min(room1.Bottom, room2.Bottom); - int yPos = top;//Rand.Range(top, bottom - width, Rand.RandSync.Server); - - return new Rectangle(left, yPos, right - left, width); - } - else if (rect1.Y > rect2.Bottom || rect2.Y > rect1.Bottom) - { - int left = Math.Max(rect1.X, rect2.X); - int right = Math.Min(rect1.Right, rect2.Right); - - int top = Math.Min(rect1.Bottom, rect2.Bottom); - int bottom = Math.Max(rect1.Y, rect2.Y); - - int xPos = Rand.Range(left, right - width, Rand.RandSync.Server); - - return new Rectangle(xPos, top, width, bottom - top); - } - else - { - DebugConsole.ThrowError("wat"); - return new Rectangle(); - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index d84956923..be359698a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -18,9 +18,9 @@ namespace Barotrauma.RuinGeneration Wall, Back, Door, Hatch, Prop } - class RuinGenerationParams : ISerializableEntity + class RuinGenerationParams : OutpostGenerationParams { - public static List List + public static List RuinParams { get { @@ -34,115 +34,27 @@ namespace Barotrauma.RuinGeneration private static List paramsList; - private string filePath; - - private readonly List roomTypeList; - - public string Name => "RuinGenerationParams"; + private readonly string filePath; + + public override string Name => "RuinGenerationParams"; - [Serialize("5000,5000", false), Editable] - public Point SizeMin + + private RuinGenerationParams(XElement element, string filePath) : base(element, filePath) { - get; - set; - } - [Serialize("8000,8000", false), Editable] - public Point SizeMax - { - get; - set; + this.filePath = filePath; } - [Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] - public int RoomDivisionIterationsMin - { - get; - set; - } - - [Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] - public int RoomDivisionIterationsMax - { - get; - set; - } - - [Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)] - public float VerticalSplitProbability - { - get; - set; - } - - [Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] - public int MinSplitWidth - { - get; - set; - } - [Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] - public int MinSplitHeight - { - get; - set; - } - - [Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable] - public Vector2 RoomWidthRange - { - get; - set; - } - [Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable] - public Vector2 RoomHeightRange - { - get; - set; - } - - [Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable] - public Point CorridorWidthRange - { - get; - set; - } - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - public IEnumerable RoomTypeList - { - get { return roomTypeList; } - } - - private RuinGenerationParams(XElement element) - { - roomTypeList = new List(); - - if (element != null) - { - foreach (XElement subElement in element.Elements()) - { - roomTypeList.Add(new RuinRoom(subElement)); - } - } - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - } - - public static RuinGenerationParams GetRandom() + public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server) { if (paramsList == null) { LoadAll(); } if (paramsList.Count == 0) { DebugConsole.ThrowError("No ruin configuration files found in any content package."); - return new RuinGenerationParams(null); + return new RuinGenerationParams(null, null); } - return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)]; + return paramsList[Rand.Int(paramsList.Count, randSync)]; } private static void LoadAll() @@ -151,23 +63,24 @@ namespace Barotrauma.RuinGeneration foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) + if (doc?.Root == null) { continue; } + + foreach (XElement subElement in doc.Root.Elements()) { - mainElement = doc.Root.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); + var mainElement = subElement; + if (subElement.IsOverride()) + { + mainElement = subElement.FirstElement(); + paramsList.Clear(); + DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); + } + else if (paramsList.Any()) + { + DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); + } + var newParams = new RuinGenerationParams(mainElement, configFile.Path); + paramsList.Add(newParams); } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); - } - var newParams = new RuinGenerationParams(mainElement) - { - filePath = configFile.Path - }; - paramsList.Add(newParams); } } @@ -185,11 +98,11 @@ namespace Barotrauma.RuinGeneration NewLineOnAttributes = true }; - foreach (RuinGenerationParams generationParams in List) + foreach (RuinGenerationParams generationParams in RuinParams) { foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) { - if (configFile.Path != generationParams.filePath) continue; + if (configFile.Path != generationParams.filePath) { continue; } XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -205,298 +118,4 @@ namespace Barotrauma.RuinGeneration } } } - - class RuinRoom : ISerializableEntity - { - public enum RoomPlacement - { - Any, - First, - Last - } - - public string Name - { - get; - private set; - } - - [Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] - public float Commonness { get; private set; } - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - [Serialize(RoomPlacement.Any, false), Editable] - public RoomPlacement Placement - { - get; - set; - } - - [Serialize(0, false), Editable] - public int PlacementOffset - { - get; - set; - } - - [Serialize(false, false), Editable] - public bool IsCorridor - { - get; - set; - } - - [Serialize(1.0f, false), Editable] - public float MinWaterAmount - { - get; - set; - } - [Serialize(1.0f, false), Editable] - public float MaxWaterAmount - { - get; - set; - } - - private List entityList = new List(); - - public RuinRoom(XElement element) - { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - - Name = element.GetAttributeString("name", ""); - - if (element != null) - { - int groupIndex = 0; - LoadEntities(element, ref groupIndex); - } - - void LoadEntities(XElement element2, ref int groupIndex) - { - foreach (XElement subElement in element2.Elements()) - { - if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase)) - { - groupIndex++; - LoadEntities(subElement, ref groupIndex); - } - else - { - entityList.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex }); - } - } - } - } - - public RuinEntityConfig GetRandomEntity(RuinEntityType type, Alignment alignment) - { - var matchingEntities = entityList.FindAll(rs => - rs.Type == type && - rs.Alignment.HasFlag(alignment)); - - if (!matchingEntities.Any()) return null; - - return ToolBox.SelectWeightedRandom( - matchingEntities, - matchingEntities.Select(s => s.Commonness).ToList(), - Rand.RandSync.Server); - } - - public List GetPropList(RuinShape room, Rand.RandSync randSync) - { - Dictionary> propGroups = new Dictionary>(); - foreach (RuinEntityConfig entityConfig in entityList) - { - if (entityConfig.Type != RuinEntityType.Prop) { continue; } - if (room.Rect.Width < entityConfig.MinRoomSize.X || room.Rect.Height < entityConfig.MinRoomSize.Y) { continue; } - if (room.Rect.Width > entityConfig.MaxRoomSize.X || room.Rect.Height > entityConfig.MaxRoomSize.Y) { continue; } - if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex)) - { - propGroups[entityConfig.SingleGroupIndex] = new List(); - } - propGroups[entityConfig.SingleGroupIndex].Add(entityConfig); - } - - List props = new List(); - foreach (KeyValuePair> propGroup in propGroups) - { - if (propGroup.Key == 0) - { - props.AddRange(propGroup.Value); - } - else - { - props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]); - } - } - return props; - } - } - - class RuinEntityConfig : ISerializableEntity - { - public readonly MapEntityPrefab Prefab; - - public enum RelativePlacement - { - SameRoom, - NextRoom, - NextCorridor, - PreviousRoom, - PreviousCorridor, - FirstRoom, - FirstCorridor, - LastRoom, - LastCorridor - } - - public class EntityConnection - { - //which type of room to search for the item to connect to - //sameroom, nextroom, previousroom, firstroom and lastroom are also valid - public string RoomName - { - get; - private set; - } - - public string TargetEntityIdentifier - { - get; - private set; - } - - //Identifier of the item to run the wire from. Only needed in item assemblies to determine which item in the assembly to use. - public string SourceEntityIdentifier - { - get; - private set; - } - - //if set, the connection is done by running a wire from - //(Pair.First = the name of the connection in this item) to (Pair.Second = the name of the connection in the target item) - public Pair WireConnection - { - get; - private set; - } - - public EntityConnection(XElement element) - { - RoomName = element.GetAttributeString("roomname", ""); - TargetEntityIdentifier = element.GetAttributeString("targetentity", ""); - SourceEntityIdentifier = element.GetAttributeString("sourceentity", ""); - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase)) - { - WireConnection = new Pair( - subElement.GetAttributeString("from", ""), - subElement.GetAttributeString("to", "")); - } - } - } - } - - [Serialize(Alignment.Bottom, false), Editable] - public Alignment Alignment { get; private set; } - - [Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." + - " For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable] - public Vector2 MinOffset { get; private set; } - [Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." + - " For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable] - public Vector2 MaxOffset { get; private set; } - - [Serialize(RuinEntityType.Prop, false), Editable] - public RuinEntityType Type { get; private set; } - - [Serialize(false, false), Editable] - public bool Expand { get; private set; } - - [Serialize(RelativePlacement.SameRoom, false), Editable] - public RelativePlacement PlacementRelativeToParent { get; private set; } - - [Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] - public float Commonness { get; private set; } - - [Serialize(1, false)] - public int MinAmount { get; private set; } - [Serialize(1, false)] - public int MaxAmount { get; private set; } - - [Serialize("0,0", false)] - public Point MinRoomSize { get; private set; } - - [Serialize("100000,100000", false)] - public Point MaxRoomSize { get; private set; } - - [Serialize("", false)] - public string TargetContainer { get; private set; } - - public List EntityConnections { get; private set; } = new List(); - - - public int SingleGroupIndex; - - private readonly List childEntities = new List(); - - public IEnumerable ChildEntities - { - get { return childEntities; } - } - - public string Name => Prefab == null ? "null" : Prefab.Name; - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - public RuinEntityConfig(XElement element) - { - string name = element.GetAttributeString("prefab", ""); - Prefab = MapEntityPrefab.Find(name: null, identifier: name); - - if (Prefab == null) - { - DebugConsole.ThrowError("Loading ruin entity config failed - map entity prefab \"" + name + "\" not found."); - return; - } - - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - - int gIndex = 0; - LoadChildren(element, ref gIndex); - - void LoadChildren(XElement element2, ref int groupIndex) - { - foreach (XElement subElement in element2.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "connection": - case "entityconnection": - EntityConnections.Add(new EntityConnection(subElement)); - break; - case "chooseone": - groupIndex++; - LoadChildren(subElement, ref groupIndex); - break; - default: - childEntities.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex }); - break; - } - } - } - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 4b6b241da..6b2fdba38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -1,212 +1,16 @@ -using FarseerPhysics; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Voronoi2; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma.RuinGeneration { - abstract class RuinShape - { - protected Rectangle rect; - - public Rectangle Rect - { - get { return rect; } - } - - public int DistanceFromEntrance - { - get; - set; - } - - public Vector2 Center - { - get { return rect.Center.ToVector2(); } - } - - public RuinRoom RoomType; - - public List Walls; - - public virtual void CreateWalls() { } - - public Alignment GetLineAlignment(Line line) - { - if (line.IsHorizontal) - { - if (line.A.Y > rect.Center.Y && line.B.Y > rect.Center.Y) - { - return Alignment.Bottom; - } - else if (line.A.Y < rect.Center.Y && line.B.Y < rect.Center.Y) - { - return Alignment.Top; - } - } - else - { - if (line.A.X < rect.Center.X && line.B.X < rect.Center.X) - { - return Alignment.Left; - } - else if (line.A.X > rect.Center.X && line.B.X > rect.Center.X) - { - return Alignment.Right; - } - } - - return Alignment.Center; - } - - /// - /// Goes through all the walls of the ruin shape and clips off parts that are inside the rectangle - /// - public void SplitWalls(Rectangle rectangle) - { - List newLines = new List(); - - foreach (Line line in Walls) - { - if (!line.IsHorizontal) //vertical line - { - //line doesn't intersect the rectangle - if (rectangle.X > line.A.X || rectangle.Right < line.A.X || - rectangle.Y > line.B.Y || rectangle.Bottom < line.A.Y) - { - newLines.Add(line); - } - //line completely inside the rectangle, no need to create a wall at all - else if (line.A.Y >= rectangle.Y && line.B.Y <= rectangle.Bottom) - { - continue; - } - //point A is within the rectangle -> cut a portion from the top of the line - else if (line.A.Y >= rectangle.Y && line.A.Y <= rectangle.Bottom) - { - newLines.Add(new Line(new Vector2(line.A.X, rectangle.Bottom), line.B)); - } - //point B is within the rectangle -> cut a portion from the bottom of the line - else if (line.B.Y >= rectangle.Y && line.B.Y <= rectangle.Bottom) - { - newLines.Add(new Line(line.A, new Vector2(line.A.X, rectangle.Y))); - } - //rect is in between the lines -> split the line into two - else - { - newLines.Add(new Line(line.A, new Vector2(line.A.X, rectangle.Y))); - newLines.Add(new Line(new Vector2(line.A.X, rectangle.Bottom), line.B)); - } - } - else - { - //line doesn't intersect the rectangle - if (rectangle.X > line.B.X || rectangle.Right < line.A.X || - rectangle.Y > line.A.Y || rectangle.Bottom < line.A.Y) - { - - newLines.Add(line); - } - else if (line.A.X >= rectangle.X && line.B.X <= rectangle.Right) - { - continue; - } - //point A is within the rectangle -> cut a portion from the left side of the line - else if (line.A.X >= rectangle.X && line.A.X <= rectangle.Right) - { - newLines.Add(new Line(new Vector2(rectangle.Right, line.A.Y), line.B)); - } - //point B is within the rectangle -> cut a portion from the right side of the line - else if (line.B.X >= rectangle.X && line.B.X <= rectangle.Right) - { - newLines.Add(new Line(line.A, new Vector2(rectangle.X, line.A.Y))); - } - //rect is in between the lines -> split the line into two - else - { - newLines.Add(new Line(line.A, new Vector2(rectangle.X, line.A.Y))); - newLines.Add(new Line(new Vector2(rectangle.Right, line.A.Y), line.B)); - } - } - } - - Walls = newLines; - } - - public void MirrorX(Vector2 mirrorOrigin) - { - rect.X = (int)(mirrorOrigin.X + (mirrorOrigin.X - rect.Right)); - for (int i = 0; i < Walls.Count; i++) - { - Walls[i].A = new Vector2(mirrorOrigin.X + (mirrorOrigin.X - Walls[i].A.X), Walls[i].A.Y); - Walls[i].B = new Vector2(mirrorOrigin.X + (mirrorOrigin.X - Walls[i].B.X), Walls[i].B.Y); - - if (Walls[i].B.X < Walls[i].A.X) - { - var temp = Walls[i].A.X; - Walls[i].A.X = Walls[i].B.X; - Walls[i].B.X = temp; - } - } - } - } - - class Line - { - public Vector2 A, B; - - public float Radius; - - public bool IsHorizontal - { - get { return Math.Abs(A.Y - B.Y) < Math.Abs(A.X - B.X); } - } - - public Line(Vector2 a, Vector2 b) - { - Debug.Assert(a.X <= b.X); - Debug.Assert(a.Y <= b.Y); - - A = a; - B = b; - } - } - partial class Ruin { - private List rooms; - private List corridors; + private readonly RuinGenerationParams generationParams; - private List walls; - - private List allShapes; - - private RuinGenerationParams generationParams; - - private BTRoom entranceRoom; - - private List ruinEntities = new List(); - private List doors = new List(); - - public IEnumerable RuinEntities - { - get { return ruinEntities; } - } - - public List RuinShapes - { - get { return allShapes; } - } - - public List Walls - { - get { return walls; } - } + public List PathCells = new List(); public Rectangle Area { @@ -214,1107 +18,54 @@ namespace Barotrauma.RuinGeneration private set; } - public Ruin(VoronoiCell closestPathCell, List caveCells, RuinGenerationParams generationParams, Rectangle area, bool mirror = false) + public Submarine Submarine + { + get; + private set; + } + + public Ruin(Level level, RuinGenerationParams generationParams, Location location, Point position, bool mirror = false) + : this(level, generationParams, location.Type, position, mirror) + { + } + + public Ruin(Level level, RuinGenerationParams generationParams, LocationType locationType, Point position, bool mirror = false) { this.generationParams = generationParams; - Area = area; - corridors = new List(); - rooms = new List(); - walls = new List(); - allShapes = new List(); - Generate(closestPathCell, caveCells, area, mirror); + Generate(level, locationType, position, mirror); } - public void Generate(VoronoiCell closestPathCell, List caveCells, Rectangle area, bool mirror = false) + public void Generate(Level level, LocationType locationType, Point position, bool mirror = false) { - corridors.Clear(); - rooms.Clear(); - - int iterations = Rand.Range(generationParams.RoomDivisionIterationsMin, generationParams.RoomDivisionIterationsMax, Rand.RandSync.Server); - float verticalProbability = generationParams.VerticalSplitProbability; - - BTRoom baseRoom = new BTRoom(area); - rooms = new List { baseRoom }; - - for (int i = 0; i < iterations; i++) - { - rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth, generationParams.MinSplitHeight)); - rooms = baseRoom.GetLeaves(); - } - - foreach (BTRoom leaf in rooms) - { - leaf.Scale - ( - new Vector2( - Rand.Range(generationParams.RoomWidthRange.X, generationParams.RoomWidthRange.Y, Rand.RandSync.Server), - Rand.Range(generationParams.RoomHeightRange.X, generationParams.RoomHeightRange.Y, Rand.RandSync.Server)) - ); - } - - baseRoom.GenerateCorridors(generationParams.CorridorWidthRange.X, generationParams.CorridorWidthRange.Y, corridors); - - walls = new List(); - rooms.ForEach(leaf => { leaf.CreateWalls(); }); - - //--------------------------- - - float shortestDistance = 0.0f; - foreach (BTRoom leaf in rooms) - { - Vector2 leafPos = leaf.Rect.Center.ToVector2(); - if (mirror) - { - leafPos.X = area.Center.X + (area.Center.X - leafPos.X); - } - float distance = Vector2.Distance(leafPos, closestPathCell.Center); - if (entranceRoom == null || distance < shortestDistance) - { - entranceRoom = leaf; - shortestDistance = distance; - } - } - - rooms.Remove(entranceRoom); - - //--------------------------- - - foreach (BTRoom leaf in rooms) - { - foreach (Corridor corridor in corridors) - { - leaf.SplitWalls(corridor.Rect); - } - - walls.AddRange(leaf.Walls); - } - - foreach (Corridor corridor in corridors) - { - corridor.CreateWalls(); - - foreach (BTRoom leaf in rooms) - { - corridor.SplitWalls(leaf.Rect); - } - - foreach (Corridor corridor2 in corridors) - { - if (corridor == corridor2) continue; - corridor.SplitWalls(corridor2.Rect); - } - walls.AddRange(corridor.Walls); - } - - BTRoom.CalculateDistancesFromEntrance(entranceRoom, rooms, corridors); - GenerateRuinEntities(caveCells, area, mirror); - } - - public class RuinEntity - { - public readonly RuinEntityConfig Config; - public readonly MapEntity Entity; - public readonly MapEntity Parent; - public readonly RuinShape Room; - - public RuinEntity(RuinEntityConfig config, MapEntity entity, RuinShape room, MapEntity parent = null) - { - Config = config; - Entity = entity; - Room = room; - Parent = parent; - } - } - - private void GenerateRuinEntities(List caveCells, Rectangle ruinArea, bool mirror) - { - var entityGrid = Hull.GenerateEntityGrid(new Rectangle(ruinArea.X, ruinArea.Y + ruinArea.Height, ruinArea.Width, ruinArea.Height)); - doors.Clear(); - - allShapes = new List(rooms); - allShapes.AddRange(corridors); + Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false); + Submarine.Info.Name = $"Ruin ({level.Seed})"; + Submarine.Info.Type = SubmarineType.Ruin; + Submarine.TeamID = CharacterTeamType.None; + Submarine.SetPosition(position.ToVector2()); if (mirror) { - foreach (RuinShape shape in allShapes) - { - shape.MirrorX(ruinArea.Center.ToVector2()); - } + Submarine.FlipX(); } - int maxRoomDistanceFromEntrance = rooms.Max(s => s.DistanceFromEntrance); - int maxCorridorDistanceFromEntrance = corridors.Max(s => s.DistanceFromEntrance); + Rectangle worldBorders = Submarine.Borders; + worldBorders.Location += Submarine.WorldPosition.ToPoint(); + Area = new Rectangle(worldBorders.X, worldBorders.Y - worldBorders.Height, worldBorders.Width, worldBorders.Height); - //assign the room types for the first and last rooms - foreach (RuinRoom roomType in generationParams.RoomTypeList) + List subWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine); + int interestingPosCount = 0; + foreach (WayPoint wp in subWaypoints) { - RuinShape selectedRoom = null; - switch (roomType.Placement) - { - case RuinRoom.RoomPlacement.First: - //find the room nearest to the entrance - //there may be multiple ones, choose one that hasn't been assigned yet - selectedRoom = roomType.IsCorridor ? FindFirstRoom(corridors, r => r.RoomType == null) : FindFirstRoom(rooms, r => r.RoomType == null); - - break; - case RuinRoom.RoomPlacement.Last: - //find the room furthest to the entrance - //there may be multiple ones, choose one that hasn't been assigned yet - selectedRoom = roomType.IsCorridor ? FindLastRoom(corridors, r => r.RoomType == null) : FindLastRoom(rooms, r => r.RoomType == null); - break; - } - if (selectedRoom == null) continue; - - //step forwards/backwards from the selected room according to the placement offset - for (int i = 0; i < Math.Abs(roomType.PlacementOffset); i++) - { - selectedRoom = FindNearestRoom( - selectedRoom, - roomType.IsCorridor ? corridors : (IEnumerable)rooms, - roomType.PlacementOffset, - r => r.RoomType == null); - } - - if (selectedRoom != null) selectedRoom.RoomType = roomType; + if (wp.SpawnType != SpawnType.Enemy) { continue; } + level.PositionsOfInterest.Add(new Level.InterestingPosition(wp.WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); + interestingPosCount++; } - //go through the unassigned rooms - foreach (RuinShape room in allShapes) + if (interestingPosCount == 0) { - if (room.RoomType != null) continue; - - room.RoomType = generationParams.RoomTypeList.GetRandom(rt => - rt.IsCorridor == room is Corridor && - rt.Placement == RuinRoom.RoomPlacement.Any, - Rand.RandSync.Server); - - if (room.RoomType == null) - { - DebugConsole.ThrowError("Could not find a suitable room type for a room (is corridor: " + (room is Corridor) + ")"); - } - } - - List hullRects = new List(allShapes.Select(s => s.Rect)); - - //split intersecting hulls into multiple parts to prevent overlaps - for (int i = 0; i < hullRects.Count; i++) - { - if (hullRects[i].Width <= 0 || hullRects[i].Height <= 0) continue; - for (int j = 0; j < hullRects.Count; j++) - { - if (i == j) continue; - if (hullRects[j].Width <= 0 || hullRects[j].Height <= 0) continue; - if (!hullRects[i].Intersects(hullRects[j])) continue; - - //hull i goes through hull j vertically - if (hullRects[i].X >= hullRects[j].X && hullRects[i].Right <= hullRects[j].Right && - hullRects[i].Y <= hullRects[j].Y && hullRects[i].Bottom >= hullRects[j].Bottom) - { - Rectangle rectLeft = new Rectangle(hullRects[j].X, hullRects[j].Y, hullRects[i].X - hullRects[j].X, hullRects[j].Height); - Rectangle rectRight = new Rectangle(hullRects[i].Right, hullRects[j].Y, hullRects[j].Right - hullRects[i].Right, hullRects[j].Height); - hullRects[j] = rectLeft; - hullRects.Add(rectRight); - } - else if //hull i goes through hull j horizontally - (hullRects[i].Y >= hullRects[j].Y && hullRects[i].Bottom <= hullRects[j].Bottom && - hullRects[i].X <= hullRects[j].X && hullRects[i].Right >= hullRects[j].Right) - { - Rectangle rectBottom = new Rectangle(hullRects[j].X, hullRects[j].Y, hullRects[j].Width, hullRects[i].Y - hullRects[j].Y); - Rectangle rectTop = new Rectangle(hullRects[j].X, hullRects[i].Bottom, hullRects[j].Width, hullRects[j].Bottom - hullRects[i].Bottom); - hullRects[j] = rectBottom; - hullRects.Add(rectTop); - } - //upper side of hull i is inside hull j - else if (hullRects[j].Contains(hullRects[i].Location) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Y))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[j].Bottom, hullRects[i].Width, hullRects[i].Bottom - hullRects[j].Bottom); - } - //lower side of hull i is inside hull j - else if (hullRects[j].Contains(new Vector2(hullRects[i].X, hullRects[i].Bottom)) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[i].Width, hullRects[j].Y - hullRects[i].Y); - } - //left side of hull i is inside hull j - else if (hullRects[j].Contains(hullRects[i].Location) && hullRects[j].Contains(new Vector2(hullRects[i].X, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[j].X, hullRects[i].Y, hullRects[i].Right - hullRects[j].X, hullRects[i].Height); - } - //right side of hull i is inside hull j - else if (hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Y)) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[j].X - hullRects[i].X, hullRects[i].Height); - } - } - } - - foreach (RuinShape room in allShapes) - { - if (room.RoomType == null) continue; - //generate walls -------------------------------------------------------------- - foreach (Line wall in room.Walls) - { - var ruinEntityConfig = room.RoomType.GetRandomEntity(RuinEntityType.Wall, room.GetLineAlignment(wall)); - if (ruinEntityConfig == null) continue; - - wall.Radius = (wall.A.X == wall.B.X) ? - (ruinEntityConfig.Prefab as StructurePrefab).Size.X * 0.5f : - (ruinEntityConfig.Prefab as StructurePrefab).Size.Y * 0.5f; - - Rectangle rect = new Rectangle( - (int)(wall.A.X - wall.Radius), - (int)(wall.B.Y + wall.Radius), - (int)((wall.B.X - wall.A.X) + wall.Radius * 2.0f), - (int)((wall.B.Y - wall.A.Y) + wall.Radius * 2.0f)); - - //cut a section off from both ends of a horizontal wall to get nicer looking corners - if (wall.A.Y == wall.B.Y) - { - rect.Inflate(-32, 0); - if (rect.Width < Submarine.GridSize.X) continue; - } - - var structure = new Structure(rect, ruinEntityConfig.Prefab as StructurePrefab, null) - { - ShouldBeSaved = false - }; - structure.SetCollisionCategory(Physics.CollisionLevel); - CreateChildEntities(ruinEntityConfig, structure, room); - ruinEntities.Add(new RuinEntity(ruinEntityConfig, structure, room)); - } - - //generate backgrounds -------------------------------------------------------------- - var backgroundConfig = room.RoomType.GetRandomEntity(RuinEntityType.Back, Alignment.Center); - if (backgroundConfig != null) - { - Rectangle backgroundRect = new Rectangle(room.Rect.X, room.Rect.Y + room.Rect.Height, room.Rect.Width, room.Rect.Height); - var backgroundStructure = new Structure(backgroundRect, (backgroundConfig.Prefab as StructurePrefab), null) - { - ShouldBeSaved = false - }; - CreateChildEntities(backgroundConfig, backgroundStructure, room); - ruinEntities.Add(new RuinEntity(backgroundConfig, backgroundStructure, room)); - } - - var submarineBlocker = GameMain.World.CreateRectangle( - ConvertUnits.ToSimUnits(room.Rect.Width), - ConvertUnits.ToSimUnits(room.Rect.Height), - 1, ConvertUnits.ToSimUnits(room.Center)); - - submarineBlocker.BodyType = BodyType.Static; - submarineBlocker.CollisionCategories = Physics.CollisionWall; - submarineBlocker.CollidesWith = Physics.CollisionWall; - submarineBlocker.UserData = "ruinroom"; - - //generate doors -------------------------------------------------------------- - if (room is Corridor corridor) - { - var doorConfig = room.RoomType.GetRandomEntity(corridor.IsHorizontal ? RuinEntityType.Door : RuinEntityType.Hatch, Alignment.Center); - if (corridor != null && doorConfig != null) - { - //find all walls that are parallel to the corridor - var suitableWalls = corridor.IsHorizontal ? - corridor.Walls.FindAll(c => c.A.Y == c.B.Y) : corridor.Walls.FindAll(c => c.A.X == c.B.X); - - if (suitableWalls.Any()) - { - //choose a random wall to place the door next to - Vector2 doorPos = corridor.Center; - var wall = suitableWalls[Rand.Int(suitableWalls.Count, Rand.RandSync.Server)]; - if (corridor.IsHorizontal) - { - doorPos.X = (wall.A.X + wall.B.X) / 2.0f; - } - else - { - doorPos.Y = (wall.A.Y + wall.B.Y) / 2.0f; - } - Item doorItem = null; - if (doorConfig.Prefab is ItemPrefab itemPrefab) - { - doorItem = new Item(doorConfig.Prefab as ItemPrefab, doorPos, null) - { - ShouldBeSaved = false - }; - } - else if (doorConfig.Prefab is ItemAssemblyPrefab itemAssemblyPrefab) - { - var entities = itemAssemblyPrefab.CreateInstance(doorPos, sub: null); - foreach (MapEntity e in entities) - { - if (e is Structure) e.ShouldBeSaved = false; - if (doorItem == null && e is Item item && item.GetComponent() != null) - { - doorItem = item; - } - else - { - ruinEntities.Add(new RuinEntity(doorConfig, e, room)); - } - } - if (doorConfig.Expand) { ExpandEntities(entities); } - //make sure the door gets positioned at the correct place regardless of its position in the item assembly - if (doorItem != null) - { - Vector2 doorOffset = doorPos - doorItem.WorldPosition; - foreach (MapEntity e in entities) - { - e.Move(doorOffset); - Door doorComponent = (e as Item)?.GetComponent(); - if (doorComponent != null && !entities.Contains(doorComponent.LinkedGap)) - { - doorComponent.LinkedGap.Move(doorOffset); - } - } - } - } - else - { - DebugConsole.ThrowError("Failed to create a ruin door. Ruin entity \"" + doorConfig.Name + "\" is marked as a door but is neither an item or an item assembly."); - continue; - } - - Door door = doorItem?.GetComponent(); - if (door == null) - { - DebugConsole.ThrowError("Failed to create a ruin door. Door not found in the ruin entity \"" + doorConfig.Name + "\"."); - continue; - } - - CreateChildEntities(doorConfig, doorItem, corridor); - doors.Add(door); - ruinEntities.Add(new RuinEntity(doorConfig, doorItem, room)); - } - } - } - - //generate props -------------------------------------------------------------- - var props = room.RoomType.GetPropList(room, Rand.RandSync.Server); - foreach (RuinEntityConfig prop in props) - { - int amount = Rand.Range(prop.MinAmount, prop.MaxAmount + 1, Rand.RandSync.Server); - for (int i = 0; i < amount; i++) - { - CreateEntity(prop, room, parent: null); - } - } - } - - foreach (RuinEntity entity in ruinEntities) - { - if (!entity.Room.RoomType.IsCorridor) { continue; } - - Item item = entity.Entity as Item; - Door door = item?.GetComponent(); - if (door == null) { continue; } - - //split the hull the door is inside - for (int i = 0; i < hullRects.Count; i++) - { - Vector2 doorPos = door.Item.WorldPosition; - if (!hullRects[i].Contains(doorPos)) continue; - - if (door.IsHorizontal) - { - Rectangle rectBottom = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[i].Width, (int)doorPos.Y - hullRects[i].Y); - Rectangle rectTop = new Rectangle(hullRects[i].X, (int)doorPos.Y, hullRects[i].Width, hullRects[i].Bottom - (int)doorPos.Y); - hullRects[i] = rectBottom; - hullRects.Add(rectTop); - } - else - { - Rectangle rectLeft = new Rectangle(hullRects[i].X, hullRects[i].Y, (int)doorPos.X - hullRects[i].X, hullRects[i].Height); - Rectangle rectRight = new Rectangle((int)doorPos.X, hullRects[i].Y, hullRects[i].Right - (int)doorPos.X, hullRects[i].Height); - hullRects[i] = rectLeft; - hullRects.Add(rectRight); - } - break; - } - } - - //randomize door states (20% open on average) - foreach (Door door in doors) - { - door.IsOpen = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.2f; - } - - //create connections between all generated entities --------------------------- - foreach (RuinEntity ruinEntity in ruinEntities) - { - CreateConnections(ruinEntity); - } - - foreach (RuinEntity ruinEntity in ruinEntities) - { - if (ruinEntity.Entity is Item item) - { - foreach (ItemComponent ic in item.Components) - { - // Prevent wiring & interacting - if (ic is ConnectionPanel connectionPanel) - { - connectionPanel.Locked = true; - connectionPanel.CanBeSelected = false; - connectionPanel.Item.ShouldBeSaved = false; - } - // Hide wires - if (ic is Wire wire) - { - wire.Hidden = true; - wire.CanBeSelected = false; - wire.Item.ShouldBeSaved = false; - } - } - } - } - - //create hulls --------------------------- - foreach (Rectangle hullRect in hullRects) - { - if (hullRect.Width <= 0 || hullRect.Height <= 0) continue; - var hull = new Hull(MapEntityPrefab.Find(null, "hull"), - new Rectangle(hullRect.X, hullRect.Y + hullRect.Height, hullRect.Width, hullRect.Height), submarine: null) - { - ParentRuin = this, - ShouldBeSaved = false - }; - RuinShape room = allShapes.Find(s => s.Rect.Contains(hullRect.Center)); - if (room?.RoomType != null) - { - hull.WaterVolume = hull.Volume * Rand.Range(room.RoomType.MinWaterAmount, room.RoomType.MaxWaterAmount, Rand.RandSync.Server); - } - entityGrid.InsertEntity(hull); - } - - //create gaps between hulls --------------------------- - hullRects.Add(entranceRoom.Rect); - for (int i = 0; i < hullRects.Count; i++) - { - if (hullRects[i].Width <= 0 || hullRects[i].Height <= 0) continue; - for (int j = i + 1; j < hullRects.Count; j++) - { - Rectangle? gapRect = null; - if (Math.Abs(hullRects[i].X - hullRects[j].Right) <= 1 && hullYIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - hullRects[i].X - 3, Math.Max(hullRects[i].Y, hullRects[j].Y), - 6, Math.Min(hullRects[i].Bottom, hullRects[j].Bottom) - Math.Max(hullRects[i].Y, hullRects[j].Y)); - } - else if (Math.Abs(hullRects[i].Right - hullRects[j].X) <= 1 && hullYIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - hullRects[i].Right - 3, Math.Max(hullRects[i].Y, hullRects[j].Y), - 6, Math.Min(hullRects[i].Bottom, hullRects[j].Bottom) - Math.Max(hullRects[i].Y, hullRects[j].Y)); - } - else if (Math.Abs(hullRects[i].Y - hullRects[j].Bottom) <= 1 && hullXIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - Math.Max(hullRects[i].X, hullRects[j].X), hullRects[i].Y - 3, - Math.Min(hullRects[i].Right, hullRects[j].Right) - Math.Max(hullRects[i].X, hullRects[j].X), 6); - } - else if (Math.Abs(hullRects[i].Bottom - hullRects[j].Y) <= 1 && hullXIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - Math.Max(hullRects[i].X, hullRects[j].X), hullRects[i].Bottom - 3, - Math.Min(hullRects[i].Right, hullRects[j].Right) - Math.Max(hullRects[i].X, hullRects[j].X), 6); - } - - if (!gapRect.HasValue || gapRect.Value.Width <= 0 || gapRect.Value.Height <= 0) continue; - - //doors create their own gaps, don't create an additional one if there's a door at this - bool doorFound = false; - foreach (Item item in Item.ItemList) - { - var door = item.GetComponent(); - if (door == null) { continue; } - if (Math.Abs(door.Item.WorldPosition.X - gapRect.Value.Center.X) < 5 && - Math.Abs(door.Item.WorldPosition.Y - gapRect.Value.Center.Y) < 5) - { - doorFound = true; - break; - } - } - if (doorFound) { continue; } - - new Gap(new Rectangle(gapRect.Value.X, gapRect.Value.Y + gapRect.Value.Height, gapRect.Value.Width, gapRect.Value.Height), - isHorizontal: gapRect.Value.Height > gapRect.Value.Width, submarine: null) - { - ParentRuin = this, - ShouldBeSaved = false - }; - } - } - - foreach (RuinEntity ruinEntity in ruinEntities) - { - ruinEntity.Entity.ParentRuin = this; - } - - bool hullXIntersect(Rectangle rect1, Rectangle rect2) - { - return - (rect1.X >= rect2.X && rect1.X <= rect2.Right) || - (rect2.X >= rect1.X && rect2.X <= rect1.Right); - } - bool hullYIntersect(Rectangle rect1, Rectangle rect2) - { - return - (rect1.Y >= rect2.Y && rect1.Y <= rect2.Bottom) || - (rect2.Y >= rect1.Y && rect2.Y <= rect1.Bottom); + //make sure there's at least on PositionsOfInterest in the ruins + level.PositionsOfInterest.Add(new Level.InterestingPosition(subWaypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); } } - - private void CreateEntity(RuinEntityConfig entityConfig, RuinShape room, MapEntity parent) - { - if (room == null) return; - - int leftWallThickness = 32, rightWallThickness = 32; - int topWallThickness = 32, bottomWallThickness = 32; - foreach (Line wall in room.Walls) - { - if (wall.IsHorizontal) - { - if (wall.A.Y > room.Rect.Center.Y) - bottomWallThickness = (int)wall.Radius; - else - topWallThickness = (int)wall.Radius; - } - else - { - if (wall.A.X > room.Rect.Center.X) - rightWallThickness = (int)wall.Radius; - else - leftWallThickness = (int)wall.Radius; - } - } - - Rectangle roomBounds = new Rectangle( - room.Rect.X + leftWallThickness, - room.Rect.Y + bottomWallThickness, - room.Rect.Width - leftWallThickness - rightWallThickness, - room.Rect.Height - topWallThickness - bottomWallThickness); - - Vector2 size = Vector2.Zero; - if (entityConfig.Prefab is StructurePrefab structurePrefab) - { - size = structurePrefab.Size; - } - else if (entityConfig.Prefab is ItemPrefab itemPrefab) - { - size = itemPrefab.Size; - } - else if (entityConfig.Prefab is ItemAssemblyPrefab assemblyPrefab) - { - size = new Vector2(assemblyPrefab.Bounds.Width, assemblyPrefab.Bounds.Height); - - Vector2 boundsMin = new Vector2(-assemblyPrefab.Bounds.X, -assemblyPrefab.Bounds.Y); - Vector2 boundsMax = new Vector2(assemblyPrefab.Bounds.Right, assemblyPrefab.Bounds.Bottom); - - roomBounds = new Rectangle( - (int)(roomBounds.X + boundsMin.X), - (int)(roomBounds.Y + boundsMin.Y), - (int)(roomBounds.Width - boundsMin.X - boundsMax.X), - (int)(roomBounds.Height - boundsMin.Y - boundsMax.Y)); - } - - List potentialAnchorPositions = new List(); - if (entityConfig.Alignment.HasFlag(Alignment.Top)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Center.X, roomBounds.Bottom)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Bottom)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Center.X, roomBounds.Top)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Right)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Right, roomBounds.Center.Y)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Left)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.X, roomBounds.Center.Y)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Center) || potentialAnchorPositions.Count == 0) - { - potentialAnchorPositions.Add(roomBounds.Center.ToVector2()); - } - - Vector2 position = potentialAnchorPositions[Rand.Int(potentialAnchorPositions.Count, Rand.RandSync.Server)]; - Vector2 minPosition = new Vector2( - position.X + entityConfig.MinOffset.X * roomBounds.Width, - position.Y + entityConfig.MinOffset.Y * roomBounds.Height); - Vector2 maxPosition = new Vector2( - position.X + entityConfig.MaxOffset.X * roomBounds.Width, - position.Y + entityConfig.MaxOffset.Y * roomBounds.Height); - - position = new Vector2( - Rand.Range(minPosition.X, maxPosition.X, Rand.RandSync.Server), - Rand.Range(minPosition.Y, maxPosition.Y, Rand.RandSync.Server)); - position.X = MathHelper.Clamp(position.X, roomBounds.X, roomBounds.Right); - position.Y = MathHelper.Clamp(position.Y, roomBounds.Y, roomBounds.Bottom); - - int iterations = 0; - while (iterations < 100) - { - bool overlapFound = false; - foreach (RuinEntity ruinEntity in ruinEntities) - { - if (ruinEntity.Config.Type == RuinEntityType.Back || ruinEntity.Config.Type == RuinEntityType.Wall) continue; - Vector2 diff = position - ruinEntity.Entity.Position; - if (Math.Abs(diff.X) < (size.X + ruinEntity.Entity.Rect.Width) / 2 && - Math.Abs(diff.Y) < (size.Y + ruinEntity.Entity.Rect.Height) / 2) - { - float dist = diff.Length(); - Vector2 moveDir = dist < 0.01f ? Vector2.UnitY : diff / dist; - - position += moveDir * 100.0f; - - position.X = MathHelper.Clamp(position.X, roomBounds.X, roomBounds.Right); - position.Y = MathHelper.Clamp(position.Y, roomBounds.Y, roomBounds.Bottom); - overlapFound = true; - } - } - iterations++; - if (!overlapFound) { break; } - } - - MapEntity entity = null; - if (entityConfig.Prefab is ItemPrefab) - { - Item container = null; - if (entityConfig.TargetContainer != "") - { - List roomContents = ruinEntities.FindAll(re => re.Room == room); - for (int j = 0; j < roomContents.Count; j++) - { - if (roomContents[j].Entity is Item && (roomContents[j].Entity as Item).HasTag(entityConfig.TargetContainer)) - { - container = roomContents[j].Entity as Item; - break; - } - } - - if (container == null) DebugConsole.ThrowError("No container with tag \"" + entityConfig.TargetContainer + "\" found, placing item in the room"); - } - - if (container != null) - { - entity = new Item((ItemPrefab)entityConfig.Prefab, container.Position, null); - if (container.OwnInventory.TryPutItem(entity as Item, null, createNetworkEvent: false)) - { - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - else // Removing items that don't fit in the container - { - entity.Remove(); - } - } - else - { - entity = new Item((ItemPrefab)entityConfig.Prefab, position, null); - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - } - else if (entityConfig.Prefab is ItemAssemblyPrefab itemAssemblyPrefab) - { - var entities = itemAssemblyPrefab.CreateInstance(position, sub: null); - foreach (MapEntity e in entities) - { - if (e is Structure) - { - e.ShouldBeSaved = false; - } - else if (e is Item item) - { - var door = item.GetComponent(); - if (door != null) { doors.Add(door); } - } - ruinEntities.Add(new RuinEntity(entityConfig, e, room, parent)); - } - if (entityConfig.Expand) - { - ExpandEntities(entities); - } - CreateChildEntities(entityConfig, entity, room); - } - else - { - entity = new Structure(new Rectangle( - (int)(position.X - size.X / 2.0f), (int)(position.Y + size.Y / 2.0f), - (int)size.X, (int)size.Y), - entityConfig.Prefab as StructurePrefab, null) - { - ShouldBeSaved = false - }; - if (entityConfig.Expand) - { - ExpandEntities(new List() { entity }); - } - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - } - - private void CreateChildEntities(RuinEntityConfig parentEntityConfig, MapEntity parentEntity, RuinShape room, Rand.RandSync randSync = Rand.RandSync.Server) - { - Dictionary> propGroups = new Dictionary>(); - foreach (RuinEntityConfig entityConfig in parentEntityConfig.ChildEntities) - { - if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex)) - { - propGroups[entityConfig.SingleGroupIndex] = new List(); - } - propGroups[entityConfig.SingleGroupIndex].Add(entityConfig); - } - - List props = new List(); - foreach (KeyValuePair> propGroup in propGroups) - { - if (propGroup.Key == 0) - { - props.AddRange(propGroup.Value); - } - else - { - props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]); - } - } - - foreach (RuinEntityConfig childEntity in props) - { - var childRoom = FindRoom(childEntity.PlacementRelativeToParent, room); - if (childRoom != null) - { - int amount = Rand.Range(childEntity.MinAmount, childEntity.MaxAmount + 1, Rand.RandSync.Server); - for (int i = 0; i < amount; i++) - { - CreateEntity(childEntity, childRoom, parentEntity); - } - } - } - } - - private void CreateConnections(RuinEntity entity) - { - foreach (RuinEntityConfig.EntityConnection connection in entity.Config.EntityConnections) - { - if (!string.IsNullOrEmpty(connection.SourceEntityIdentifier) && - connection.SourceEntityIdentifier != entity.Entity?.prefab.Identifier) - { - continue; - } - - MapEntity targetEntity = null; - if (connection.TargetEntityIdentifier == "parent") - { - targetEntity = entity.Parent; - } - else if (!string.IsNullOrEmpty(connection.RoomName)) - { - RuinShape targetRoom = null; - if (Enum.TryParse(connection.RoomName, out RuinEntityConfig.RelativePlacement placement)) - { - targetRoom = FindRoom(placement, entity.Room); - } - else - { - targetRoom = allShapes.Find(s => s.RoomType?.Name == connection.RoomName); - } - - if (targetRoom == null) - { - DebugConsole.ThrowError("Error while generating ruins - could not find a room of the type \"" + connection.RoomName + "\"."); - } - else - { - targetEntity = ruinEntities.GetRandom(e => - e.Room == targetRoom && - e.Entity.prefab?.Identifier == connection.TargetEntityIdentifier, Rand.RandSync.Server)?.Entity; - } - } - else - { - targetEntity = ruinEntities.GetRandom(e => e.Entity.prefab?.Identifier == connection.TargetEntityIdentifier, Rand.RandSync.Server)?.Entity; - } - - if (targetEntity == null) continue; - - if (connection.WireConnection != null) - { - Item item = entity.Entity as Item; - if (item == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + entity.Entity.Name + "\" - the entity is not an item."); - continue; - } - else if (item.Connections == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + entity.Entity.Name + "\" - the item does not have a connection panel component."); - continue; - } - - Item parentItem = entity.Parent as Item; - if (parentItem == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + "\" - the entity is not an item."); - continue; - } - else if (parentItem.Connections == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + "\" - the item does not have a connection panel component."); - continue; - } - - //TODO: alien wire prefab w/ custom sprite? - var wirePrefab = MapEntityPrefab.Find(null, "blackwire") as ItemPrefab; - - var conn1 = item.Connections.Find(c => c.Name == connection.WireConnection.First); - if (conn1 == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + item.Name + - "\" - the item does not have a connection named \"" + connection.WireConnection.First + "\"."); - continue; - } - var conn2 = parentItem.Connections.Find(c => c.Name == connection.WireConnection.Second); - if (conn2 == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + - "\" - the item does not have a connection named \"" + connection.WireConnection.Second + "\"."); - continue; - } - - var wire = new Item(wirePrefab, parentItem.WorldPosition, null).GetComponent(); - wire.Item.ShouldBeSaved = false; - conn1.TryAddLink(wire); - wire.Connect(conn1, true); - conn2.TryAddLink(wire); - wire.Connect(conn2, true); - wire.Hidden = true; // Hidden for now - } - else - { - entity.Entity.linkedTo.Add(targetEntity); - targetEntity.linkedTo.Add(entity.Entity); - } - } - } - - private void ExpandEntities(IEnumerable entities) - { - Vector2 xBounds = new Vector2(entities.Min(e => e.Rect.X), entities.Max(e => e.Rect.Right)); - Vector2 yBounds = new Vector2(entities.Min(e => e.Rect.Y - e.Rect.Height), entities.Max(e => e.Rect.Y)); - Vector2 center = new Vector2((xBounds.X + xBounds.Y) / 2.0f, (yBounds.X + yBounds.Y) / 2.0f); - - foreach (MapEntity entity in entities) - { - if (entity is Item item) - { - Vector2 moveTo = StretchPoint(entity.WorldPosition, center, xBounds, yBounds); - Vector2 moveAmount = moveTo - entity.WorldPosition; - var connectionPanel = item.GetComponent(); - connectionPanel?.MoveConnectedWires(moveAmount); - entity.Move(moveAmount); - } - else if (entity is Structure structure) - { - if (!entity.ResizeHorizontal && !entity.ResizeVertical) - { - Vector2 moveTo = StretchPoint(entity.WorldPosition, center, xBounds, yBounds); - entity.Move(moveTo - entity.WorldPosition); - continue; - } - - Vector2 structureBoundsMin = new Vector2(structure.Rect.X, structure.Rect.Y - structure.Rect.Height); - Vector2 structureBoundsMax = new Vector2(structure.Rect.Right, structure.Rect.Y); - - if (structure.ResizeHorizontal) - { - if (structure.Rect.Right > center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structureBoundsMax.X, structure.Rect.Y - structure.Rect.Height / 2), - new Vector2(center.X, structure.Rect.Y - structure.Rect.Height / 2), - xBounds, yBounds); - structureBoundsMax.X = moveTo.X; - } - if (structure.Rect.X < center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structureBoundsMin.X, structure.Rect.Y - structure.Rect.Height / 2), - new Vector2(center.X, structure.Rect.Y - structure.Rect.Height / 2), - xBounds, yBounds); - structureBoundsMin.X = moveTo.X; - } - } - if (structure.ResizeVertical) - { - if (structure.Rect.Y > center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structure.Rect.Center.X, structureBoundsMax.Y), - new Vector2(structure.Rect.Center.X, center.Y), - xBounds, yBounds); - structureBoundsMax.Y = moveTo.Y; - } - if (structure.Rect.Y - structure.Rect.Height < center.Y) - { - Vector2 moveTo = StretchPoint( - new Vector2(structure.Rect.Center.X, structureBoundsMin.Y), - new Vector2(structure.Rect.Center.X, center.Y), - xBounds, yBounds); - structureBoundsMin.Y = moveTo.Y; - } - } - - structure.Rect = new Rectangle( - (int)structureBoundsMin.X, - (int)structureBoundsMax.Y, - (int)(structureBoundsMax.X - structureBoundsMin.X), - (int)(structureBoundsMax.Y - structureBoundsMin.Y)); - } - } - } - - private Vector2 StretchPoint(Vector2 point, Vector2 center, Vector2 xBounds, Vector2 yBounds) - { - Vector2 diff = point - center; - if (diff.LengthSquared() < 0.0001f) return point; - - Vector2? closestIntersection = RayCastWalls(point, Vector2.Normalize(diff)); - - if (!closestIntersection.HasValue) return point; - - Vector2 moveAmount = closestIntersection.Value - point; - Vector2 moveRatio = new Vector2( - Math.Abs(diff.X) / ((xBounds.Y - xBounds.X) * 0.5f), - Math.Abs(diff.Y) / ((yBounds.Y - yBounds.X) * 0.5f)); - return point + new Vector2(moveAmount.X * moveRatio.X, moveAmount.Y * moveRatio.Y); - } - - private Vector2? RayCastWalls(Vector2 worldPosition, Vector2 dir) - { - float rayLength = 10000.0f; - Vector2 rayStart = worldPosition; - Vector2 rayEnd = worldPosition + dir * rayLength; - Vector2? closestIntersection = null; - float closestDist = rayLength * rayLength; - foreach (Line line in walls) - { - if (!MathUtils.GetLineIntersection(line.A, line.B, rayStart, rayEnd, out Vector2 intersection)) { continue; } - - intersection = line.IsHorizontal ? - new Vector2(intersection.X, intersection.Y - Math.Sign(dir.Y) * line.Radius) : - new Vector2(intersection.X - Math.Sign(dir.X) * line.Radius, intersection.Y); - - float dist = Vector2.DistanceSquared(rayStart, intersection); - if (dist < closestDist) - { - closestIntersection = intersection; - closestDist = dist; - } - } - return closestIntersection; - } - - private RuinShape FindRoom(RuinEntityConfig.RelativePlacement placement, RuinShape relativeTo) - { - switch (placement) - { - case RuinEntityConfig.RelativePlacement.SameRoom: - return relativeTo; - case RuinEntityConfig.RelativePlacement.NextRoom: - return FindNearestRoom(relativeTo, rooms, 1); - case RuinEntityConfig.RelativePlacement.NextCorridor: - return FindNearestRoom(relativeTo, corridors, 1); - case RuinEntityConfig.RelativePlacement.PreviousRoom: - return FindNearestRoom(relativeTo, rooms, -1); - case RuinEntityConfig.RelativePlacement.PreviousCorridor: - return FindNearestRoom(relativeTo, corridors, -1); - case RuinEntityConfig.RelativePlacement.FirstRoom: - return FindFirstRoom(rooms); - case RuinEntityConfig.RelativePlacement.FirstCorridor: - return FindFirstRoom(corridors); - case RuinEntityConfig.RelativePlacement.LastRoom: - return FindLastRoom(rooms); - case RuinEntityConfig.RelativePlacement.LastCorridor: - return FindLastRoom(corridors); - default: - throw new NotImplementedException(); - } - } - - /// - /// Find the nearest room relative to a specific room. - /// - /// The room to compare the distance with - /// List of rooms to check (use a list that only contains rooms/corridors if you want a specific types of rooms) - /// Direction to check: 1 = find the next room, -1 = find the previous room - private RuinShape FindNearestRoom(RuinShape relativeTo, IEnumerable roomList, int dir, Func predicate = null) - { - dir = Math.Sign(dir); - RuinShape selectedRoom = null; - foreach (RuinShape room in roomList) - { - if (room == relativeTo) continue; - if (predicate != null && !predicate(room)) continue; - int roomDir = Math.Sign(room.DistanceFromEntrance - relativeTo.DistanceFromEntrance); - - if (roomDir == 0 || roomDir == dir) - { - if (selectedRoom == null) - { - selectedRoom = room; - } - else //room already selected, check if this one is closer - { - //closer than the previously selected room - if (Math.Abs(room.DistanceFromEntrance - relativeTo.DistanceFromEntrance) < - Math.Abs(selectedRoom.DistanceFromEntrance - relativeTo.DistanceFromEntrance)) - { - selectedRoom = room; - } - //same distance measured in room indices, select the room if the actual distance is smaller - else if (room.DistanceFromEntrance == selectedRoom.DistanceFromEntrance && - Vector2.DistanceSquared(relativeTo.Center, room.Center) < Vector2.DistanceSquared(relativeTo.Center, selectedRoom.Center)) - { - selectedRoom = room; - } - } - } - } - return selectedRoom; - } - - private RuinShape FindFirstRoom(IEnumerable roomList, Func predicate = null) - { - if (!roomList.Any()) { return null; } - RuinShape firstRoom = null; - foreach (RuinShape room in roomList) - { - if (predicate != null && !predicate(room)) continue; - if (firstRoom == null || room.DistanceFromEntrance < firstRoom.DistanceFromEntrance) - { - firstRoom = room; - } - } - return firstRoom; - } - - private RuinShape FindLastRoom(IEnumerable roomList, Func predicate = null) - { - if (!roomList.Any()) { return null; } - RuinShape lastRoom = null; - foreach (RuinShape room in roomList) - { - if (predicate != null && !predicate(room)) continue; - if (lastRoom == null || room.DistanceFromEntrance > lastRoom.DistanceFromEntrance) - { - lastRoom = room; - } - } - return lastRoom; - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 153bfa954..6c208d35b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -60,7 +60,7 @@ namespace Barotrauma private LocationType addInitialMissionsForType; - public bool Discovered; + public bool Discovered { get; private set; } public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; @@ -90,7 +90,7 @@ namespace Barotrauma private const float StoreMaxReputationModifier = 0.1f; private const float StoreSellPriceModifier = 0.8f; - private const float DailySpecialPriceModifier = 0.9f; + private const float DailySpecialPriceModifier = 0.5f; private const float RequestGoodPriceModifier = 1.5f; public const int StoreInitialBalance = 5000; /// @@ -868,6 +868,8 @@ namespace Barotrauma // Adjust by random price modifier price = ((100 + StorePriceModifier) / 100.0f) * price; + price *= priceInfo.BuyingPriceMultiplier; + // Adjust by daily special status if (considerDailySpecials && DailySpecials.Contains(item)) { @@ -1004,12 +1006,22 @@ namespace Barotrauma stockToRemove.ForEach(i => stock.Remove(i)); StoreStock = stock; - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval) + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + + if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || + DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) { CreateStoreSpecials(); } } + private int GetExtraSpecialSalesCount() + { + var characters = GameSession.GetSessionCrewCharacters(); + if (!characters.Any()) { return 0; } + return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); + } + private void GenerateRandomPriceModifier() { StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange); @@ -1033,7 +1045,9 @@ namespace Barotrauma } availableStock.Add(stockItem.ItemPrefab, weight); } - for (int i = 0; i < DailySpecialsCount; i++) + + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); @@ -1111,6 +1125,30 @@ namespace Barotrauma return nextStatus; } + public void Discover(bool checkTalents = true) + { + if (Discovered) { return; } + Discovered = true; + if (checkTalents) + { + GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Abilities.AbilityLocation(this))); + } + } + + public void Reset() + { + if (Type != OriginalType) + { + ChangeType(OriginalType); + PendingLocationTypeChange = null; + } + CreateStore(force: true); + ClearMissions(); + LevelData?.EventHistory?.Clear(); + UnlockInitialMissions(); + Discovered = false; + } + public XElement Save(Map map, XElement parentElement) { var locationElement = new XElement("location", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index e49683e88..53342d1aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -231,7 +231,7 @@ namespace Barotrauma } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - CurrentLocation.Discovered = true; + CurrentLocation.Discover(true); CurrentLocation.CreateStore(); InitProjectSpecific(); @@ -472,17 +472,18 @@ namespace Barotrauma foreach (LocationConnection connection in Connections) { - connection.Difficulty = MathHelper.Clamp((connection.CenterPos.X / Width * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); + float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width); + connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f); } AssignBiomes(); CreateEndLocation(); - + foreach (Location location in Locations) { location.LevelData = new LevelData(location) { - Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f) + Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f) }; location.UnlockInitialMissions(); } @@ -490,6 +491,14 @@ namespace Barotrauma { connection.LevelData = new LevelData(connection); } + + float GetLevelDifficulty(float areaDifficulty) + { + const float CurveModifier = 1.5f; + const float DifficultyMultiplier = 1.14f; + const float BaseDifficulty = -3f; + return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty; + } } partial void GenerateLocationConnectionVisuals(); @@ -671,7 +680,7 @@ namespace Barotrauma SelectedConnection.Passed = true; CurrentLocation = SelectedLocation; - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); SelectedLocation = null; CurrentLocation.CreateStore(); @@ -702,7 +711,7 @@ namespace Barotrauma Location prevLocation = CurrentLocation; CurrentLocation = Locations[index]; - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); if (prevLocation != CurrentLocation) { @@ -1055,7 +1064,10 @@ namespace Barotrauma } } location.LoadLocationTypeChange(subElement); - location.Discovered = subElement.GetAttributeBool("discovered", false); + if (subElement.GetAttributeBool("discovered", false)) + { + location.Discover(checkTalents: false); + } if (location.Discovered) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index fbe5d775f..766ef959e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -149,7 +149,7 @@ namespace Barotrauma public XElement Save() { XElement element = new XElement(nameof(Radiation)); - SerializableProperty.SerializeProperties(this, element); + SerializableProperty.SerializeProperties(this, element, saveIfDefault: true); return element; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 61e14591b..b14ee383e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -48,8 +48,7 @@ namespace Barotrauma } } - //observable collection because some entities may need to be notified when the collection is modified - public readonly ObservableCollection linkedTo = new ObservableCollection(); + public readonly List linkedTo = new List(); protected bool flippedX, flippedY; public bool FlippedX { get { return flippedX; } } @@ -225,12 +224,6 @@ namespace Barotrauma } } - public RuinGeneration.Ruin ParentRuin - { - get; - set; - } - [Serialize(true, true)] public bool RemoveIfLinkedOutpostDoorInUse { @@ -416,14 +409,13 @@ namespace Barotrauma //connect clone wires to the clone items and refresh links between doors and gaps for (int i = 0; i < clones.Count; i++) { - var cloneItem = clones[i] as Item; - if (cloneItem == null) { continue; } + if (!(clones[i] is Item cloneItem)) { continue; } var door = cloneItem.GetComponent(); door?.RefreshLinkedGap(); var cloneWire = cloneItem.GetComponent(); - if (cloneWire == null) continue; + if (cloneWire == null) { continue; } var originalWire = ((Item)entitiesToClone[i]).GetComponent(); @@ -431,10 +423,23 @@ namespace Barotrauma for (int n = 0; n < 2; n++) { - if (originalWire.Connections[n] == null) { continue; } + if (originalWire.Connections[n] == null) + { + var disconnectedFrom = entitiesToClone.Find(e => e is Item item && (item.GetComponent()?.DisconnectedWires.Contains(originalWire) ?? false)); + if (disconnectedFrom == null) { continue; } + + int disconnectedFromIndex = entitiesToClone.IndexOf(disconnectedFrom); + var disconnectedFromClone = (clones[disconnectedFromIndex] as Item)?.GetComponent(); + if (disconnectedFromClone == null) { continue; } + + disconnectedFromClone.DisconnectedWires.Add(cloneWire); + if (cloneWire.Item.body != null) { cloneWire.Item.body.Enabled = false; } + cloneWire.IsActive = false; + continue; + } var connectedItem = originalWire.Connections[n].Item; - if (connectedItem == null) continue; + if (connectedItem == null) { continue; } //index of the item the wire is connected to int itemIndex = entitiesToClone.IndexOf(connectedItem); @@ -515,7 +520,11 @@ namespace Barotrauma } #endif - if (aiTarget != null) aiTarget.Remove(); + if (aiTarget != null) + { + aiTarget.Remove(); + aiTarget = null; + } if (linkedTo != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 577750efd..7b29db634 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public static List Params { get; private set; } - public string Name { get; private set; } + public virtual string Name { get; private set; } public string Identifier { get; private set; } @@ -67,6 +67,34 @@ namespace Barotrauma set; } + [Serialize(true, isSaveable: true), Editable] + public bool LockUnusedDoors + { + get; + set; + } + + [Serialize(true, isSaveable: true), Editable] + public bool RemoveUnusedGaps + { + get; + set; + } + + [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float MinWaterPercentage + { + get; + set; + } + + [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float MaxWaterPercentage + { + get; + set; + } + [Serialize("", isSaveable: true), Editable] public string ReplaceInRadiation { get; set; } @@ -81,12 +109,14 @@ namespace Barotrauma public Dictionary SerializableProperties { get; private set; } - private OutpostGenerationParams(XElement element, string filePath) + protected OutpostGenerationParams(XElement element, string filePath) { Identifier = element.GetAttributeString("identifier", ""); Name = element.GetAttributeString("name", Identifier); allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty()).ToList(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + if (element == null) { return; } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 2e07a4c11..3b0dfa429 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -77,7 +77,7 @@ namespace Barotrauma locationType = location.GetLocationType(); } - + //load the infos of the outpost module files List outpostModules = new List(); foreach (ContentFile outpostModuleFile in outpostModuleFiles) @@ -85,6 +85,19 @@ namespace Barotrauma var subInfo = new SubmarineInfo(outpostModuleFile.Path); if (subInfo.OutpostModuleInfo != null) { + if (generationParams is RuinGeneration.RuinGenerationParams) + { + //if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins + if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") && + !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key))) + { + continue; + } + } + else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin")) + { + continue; + } outpostModules.Add(subInfo); } } @@ -162,7 +175,7 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); - AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType); + AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase))) { remainingTries--; @@ -233,17 +246,23 @@ namespace Barotrauma var selectedModule = selectedModules[i]; sub.Info.GameVersion = selectedModule.Info.GameVersion; var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset); - idOffset = moduleEntities.Max(e => e.ID); + MapEntity.InitializeLoadedLinks(moduleEntities); - foreach (MapEntity entity in moduleEntities) + foreach (MapEntity entity in moduleEntities.ToList()) { entity.OriginalModuleIndex = i; if (!(entity is Item item)) { continue; } - item.GetComponent()?.RefreshLinkedGap(); + var door = item.GetComponent(); + if (door != null) + { + door.RefreshLinkedGap(); + if (!moduleEntities.Contains(door.LinkedGap)) { moduleEntities.Add(door.LinkedGap); } + } item.GetComponent()?.InitializeLinks(); item.GetComponent()?.OnMapLoaded(); } + idOffset = moduleEntities.Max(e => e.ID); var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); @@ -345,11 +364,33 @@ namespace Barotrauma Submarine.RepositionEntities(module.Offset + sub.HiddenSubPosition, entities[module]); } Gap.UpdateHulls(); - allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities)); + allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities, generationParams is RuinGeneration.RuinGenerationParams)); LinkOxygenGenerators(allEntities); - LockUnusedDoors(selectedModules, entities); + if (generationParams.LockUnusedDoors) + { + LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps); + } AlignLadders(selectedModules, entities); PowerUpOutpost(entities.SelectMany(e => e.Value)); + if (generationParams.MaxWaterPercentage > 0.0f) + { + foreach (var entity in allEntities) + { + if (entity is Hull hull) + { + float diff = generationParams.MaxWaterPercentage - generationParams.MinWaterPercentage; + if (diff < 0.01f) + { + // Overfill the hulls to get rid of air pockets in the vertical hallways. Airpockets make it impossible to swim up the hallways. + hull.WaterVolume = hull.Volume * 2; + } + else + { + hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f; + } + } + } + } } return allEntities; @@ -414,7 +455,8 @@ namespace Barotrauma List pendingModuleFlags, List selectedModules, LocationType locationType, - bool retry = true) + bool retry = true, + bool allowExtendBelowInitialModule = false) { if (pendingModuleFlags.Count == 0) { return true; } @@ -422,8 +464,11 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) { var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); @@ -438,7 +483,7 @@ namespace Barotrauma //try to append to some other module first foreach (PlacedModule otherModule in selectedModules) { - if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false)) + if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { return true; } @@ -454,7 +499,7 @@ namespace Barotrauma //retry currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); if (currentModule == null) { break; } - if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false)) + if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { return true; } @@ -676,6 +721,10 @@ namespace Barotrauma else { availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical") + { + availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical")); + } } if (availableModules.Count() == 0) { return null; } @@ -840,7 +889,7 @@ namespace Barotrauma return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s)); } - private static List GenerateHallways(Submarine sub, LocationType locationType, IEnumerable placedModules, IEnumerable availableModules, Dictionary> allEntities) + private static List GenerateHallways(Submarine sub, LocationType locationType, IEnumerable placedModules, IEnumerable availableModules, Dictionary> allEntities, bool isRuin) { //if a hallway is shorter than this, one of the doors at the ends of the hallway is removed const float MinTwoDoorHallwayLength = 32.0f; @@ -1193,14 +1242,13 @@ namespace Barotrauma } } - private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities) + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) { foreach (MapEntity me in entities[module]) { - var gap = me as Gap; - if (gap == null) { continue; } + if (!(me is Gap gap)) { continue; } var door = gap.ConnectedDoor; if (door != null && !door.UseBetweenOutpostModules) { continue; } if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap)) @@ -1247,11 +1295,11 @@ namespace Barotrauma if (connectionPanel != null) { connectionPanel.Locked = true; } } } - else + else if (removeUnusedGaps) { gap.Remove(); WayPoint.WayPointList.Where(wp => wp.ConnectedGap == gap).ForEachMod(wp => wp.Remove()); - } + } } entities[module].RemoveAll(e => e.Removed); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 4b0307e7e..c0c051586 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -89,11 +89,13 @@ namespace Barotrauma if (newFlags.Contains("hallwayhorizontal")) { moduleFlags.Add("hallwayhorizontal"); + if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } return; } if (newFlags.Contains("hallwayvertical")) { moduleFlags.Add("hallwayvertical"); + if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } return; } if (!newFlags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 78a7e14f3..25dff440e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -24,6 +24,11 @@ namespace Barotrauma /// The item isn't available in stores unless the level's difficulty is above this value /// public readonly int MinLevelDifficulty; + /// + /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. + /// + public readonly float BuyingPriceMultiplier = 1f; + /// /// Support for the old style of determining item prices @@ -34,6 +39,7 @@ namespace Barotrauma { Price = element.GetAttributeInt("buyprice", 0); MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); + BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); CanBeBought = true; var minAmount = GetMinAmount(element); MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); @@ -42,11 +48,12 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f) { Price = price; CanBeBought = canBeBought; MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); + BuyingPriceMultiplier = buyingPriceMultiplier; maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, minAmount); MinLevelDifficulty = minLevelDifficulty; @@ -62,6 +69,7 @@ namespace Barotrauma var maxAmount = GetMaxAmount(element); var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); var canBeSpecial = element.GetAttributeBool("canbespecial", true); + var buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); var priceInfos = new List>(); foreach (XElement childElement in element.GetChildElements("price")) @@ -73,7 +81,7 @@ namespace Barotrauma minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty)))); + childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier)))); } var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); @@ -81,7 +89,7 @@ namespace Barotrauma minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, canBeSpecial, - minLevelDifficulty); + minLevelDifficulty, buyingPriceMultiplier); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 23644df10..2907db5ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -8,8 +8,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; +using Barotrauma.Lights; #endif namespace Barotrauma @@ -47,7 +49,7 @@ namespace Barotrauma const float LeakThreshold = 0.1f; #if CLIENT - private SpriteEffects SpriteEffects = SpriteEffects.None; + public SpriteEffects SpriteEffects = SpriteEffects.None; #endif //dimensions of the wall sections' physics bodies (only used for debug rendering) @@ -96,7 +98,7 @@ namespace Barotrauma { get { return Prefab.Body; } } - + public List Bodies { get; private set; } public bool CastShadow @@ -112,7 +114,7 @@ namespace Barotrauma } private float? maxHealth; - + [Serialize(100.0f, true)] public float MaxHealth { @@ -167,7 +169,7 @@ namespace Barotrauma { get { return prefab.Tags; } } - + protected Color spriteColor; [Editable, Serialize("1.0,1.0,1.0,1.0", true)] public Color SpriteColor @@ -175,7 +177,7 @@ namespace Barotrauma get { return spriteColor; } set { spriteColor = value; } } - + [Editable, Serialize(false, true)] public bool UseDropShadow { @@ -203,8 +205,8 @@ namespace Barotrauma if (!ResizeHorizontal || !ResizeVertical) { - int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale); - int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale); + int newWidth = Math.Max(ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale), 1); + int newHeight = Math.Max(ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale), 1); Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight); if (StairDirection != Direction.None) { @@ -215,6 +217,13 @@ namespace Barotrauma UpdateSections(); } } + +#if CLIENT + foreach (LightSource light in Lights) + { + light.SpriteScale = scale * textureScale; + } +#endif } } @@ -230,6 +239,13 @@ namespace Barotrauma textureScale = new Vector2( MathHelper.Clamp(value.X, 0.01f, 10), MathHelper.Clamp(value.Y, 0.01f, 10)); + +#if CLIENT + foreach (LightSource light in Lights) + { + light.LightTextureScale = textureScale * scale; + } +#endif } } @@ -238,7 +254,13 @@ namespace Barotrauma public Vector2 TextureOffset { get { return textureOffset; } - set { textureOffset = value; } + set + { + textureOffset = value; +#if CLIENT + SetLightTextureOffset(); +#endif + } } @@ -281,10 +303,10 @@ namespace Barotrauma secRect.X += value.X; secRect.Y += value.Y; sec.rect = secRect; } - } + } } } - + public float BodyWidth { get { return Prefab.BodyWidth > 0.0f ? Prefab.BodyWidth * scale : rect.Width; } @@ -363,6 +385,12 @@ namespace Barotrauma #if CLIENT convexHulls?.ForEach(x => x.Move(amount)); + + foreach (LightSource light in Lights) + { + light.LightTextureTargetSize = rect.Size.ToVector2(); + light.Position = rect.Location.ToVector2(); + } #endif } @@ -374,7 +402,7 @@ namespace Barotrauma defaultRect = rectangle; maxHealth = sp.Health; - + rect = rectangle; TextureScale = sp.TextureScale; @@ -426,25 +454,60 @@ namespace Barotrauma if (StairDirection != Direction.None) { CreateStairBodies(); - } + } } } SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); - // Only add ai targets automatically to submarine/outpost walls +#if CLIENT + foreach (XElement subElement in sp.ConfigElement.Elements()) + { + if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase)) + { + Vector2 pos = rect.Location.ToVector2(); + pos.Y += rect.Height; + LightSource light = new LightSource(subElement) + { + ParentSub = Submarine, + Position = rect.Location.ToVector2(), + CastShadows = false, + IsBackground = false, + Color = subElement.GetAttributeColor("lightcolor", Color.White), + SpriteScale = Vector2.One, + Range = 0, + LightTextureTargetSize = rect.Size.ToVector2(), + LightTextureScale = textureScale * scale, + LightSourceParams = + { + Flicker = subElement.GetAttributeFloat("flicker", 0f), + FlickerSpeed = subElement.GetAttributeFloat("flickerspeed", 0f), + PulseAmount = subElement.GetAttributeFloat("pulseamount", 0f), + PulseFrequency = subElement.GetAttributeFloat("pulsefrequency", 0f), + BlinkFrequency = subElement.GetAttributeFloat("blinkfrequency", 0f) + } + }; + + Lights.Add(light); + + SetLightTextureOffset(); + } + } +#endif + + // Only add ai targets automatically to submarine/outpost walls if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget) { aiTarget = new AITarget(this) { - MinSightRange = 2000, - MaxSightRange = 5000, + MinSightRange = 1000, + MaxSightRange = 4000, MaxSoundRange = 0 }; } InsertToList(); - + DebugConsole.Log("Created " + Name + " (" + ID + ")"); } @@ -476,7 +539,7 @@ namespace Barotrauma { Bodies = new List(); bodyDebugDimensions.Clear(); - + float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f)); float bodyWidth = ConvertUnits.ToSimUnits(rect.Width / Math.Cos(stairAngle)); @@ -504,9 +567,9 @@ namespace Barotrauma { int xsections = 1, ysections = 1; int width = rect.Width, height = rect.Height; - + if (!HasBody) - { + { if (FlippedX && IsHorizontal) { xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width); @@ -548,8 +611,8 @@ namespace Barotrauma if (FlippedX || FlippedY) { Rectangle sectionRect = new Rectangle( - FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width, - FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height, + FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width, + FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height, width, height); if (FlippedX) @@ -645,8 +708,8 @@ namespace Barotrauma Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation); - return - Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && + return + Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f; } else @@ -692,6 +755,10 @@ namespace Barotrauma #if CLIENT if (convexHulls != null) convexHulls.ForEach(x => x.Remove()); + foreach (LightSource light in Lights) + { + light.Remove(); + } #endif } @@ -724,6 +791,10 @@ namespace Barotrauma #if CLIENT if (convexHulls != null) convexHulls.ForEach(x => x.Remove()); + foreach (LightSource light in Lights) + { + light.Remove(); + } #endif } @@ -781,7 +852,7 @@ namespace Barotrauma return (IsHorizontal ? Sections[sectionIndex].rect.Width : Sections[sectionIndex].rect.Height); } - + public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { if (!upgrade.Prefab.IsWallUpgrade) { return false; } @@ -799,7 +870,7 @@ namespace Barotrauma Upgrades.Add(upgrade); upgrade.ApplyUpgrade(); } - + UpdateSections(); return true; @@ -914,7 +985,7 @@ namespace Barotrauma { diffFromCenter = -diffFromCenter; } - + Vector2 sectionPos = Position + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; @@ -925,7 +996,7 @@ namespace Barotrauma } return sectionPos; } - } + } public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) { @@ -948,13 +1019,28 @@ namespace Barotrauma GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); #endif } - } + } #if CLIENT if (playSound && damageAmount > 0) { - SoundPlayer.PlayDamageSound(attack.StructureSoundType, damageAmount, worldPosition, tags: Tags); + string damageSound = Prefab.DamageSound; + if (string.IsNullOrWhiteSpace(damageSound)) + { + damageSound = attack.StructureSoundType; + } + SoundPlayer.PlayDamageSound(damageSound, damageAmount, worldPosition, tags: Tags); } #endif + + if (Submarine != null && damageAmount > 0 && attacker != null) + { + var abilityAttackerSubmarine = new AbilityCharacterSubmarine(attacker, Submarine); + foreach (Character character in Character.CharacterList) + { + character.CheckTalents(AbilityEffectType.AfterSubmarineAttacked, abilityAttackerSubmarine); + } + } + return new AttackResult(damageAmount, null); } @@ -965,7 +1051,7 @@ namespace Barotrauma if (!MathUtils.IsValid(damage)) { return; } damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth); - + #if SERVER if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage) { @@ -1011,10 +1097,10 @@ namespace Barotrauma { diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth; if (BodyWidth > 0.0f) { gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width)); } - if (BodyHeight > 0.0f) - { + if (BodyHeight > 0.0f) + { gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale); - gapRect.Height = (int)BodyHeight; + gapRect.Height = (int)BodyHeight; } } else @@ -1023,7 +1109,7 @@ namespace Barotrauma if (BodyWidth > 0.0f) { gapRect.X = gapRect.Center.X + (int)(-BodyWidth / 2 + BodyOffset.X * scale); - gapRect.Width = (int)BodyWidth; + gapRect.Width = (int)BodyWidth; } if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); } } @@ -1042,7 +1128,7 @@ namespace Barotrauma gapRect.Y += 10; gapRect.Width += 20; gapRect.Height += 20; - + bool horizontalGap = !IsHorizontal; if (Prefab.BodyRotation != 0.0f) { @@ -1079,7 +1165,7 @@ namespace Barotrauma } float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); - Sections[sectionIndex].gap.Open = gapOpen; + Sections[sectionIndex].gap.Open = gapOpen; } float damageDiff = damage - Sections[sectionIndex].damage; @@ -1095,9 +1181,8 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical", - -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f), - SectionPosition(sectionIndex)); + attacker.Info?.IncreaseSkillLevel("mechanical", + -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f)); } } } @@ -1105,7 +1190,7 @@ namespace Barotrauma bool hasHole = SectionBodyDisabled(sectionIndex); if (hadHole == hasHole) { return; } - + UpdateSections(); } @@ -1187,7 +1272,7 @@ namespace Barotrauma if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1); } if (FlippedX) { diffFromCenter = -diffFromCenter; } - + Vector2 bodyOffset = ConvertUnits.ToSimUnits(Prefab.BodyOffset) * scale; if (FlippedX) { bodyOffset.X = -bodyOffset.X; } if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; } @@ -1229,7 +1314,7 @@ namespace Barotrauma } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation); - + public override void FlipX(bool relativeToSub) { base.FlipX(relativeToSub); @@ -1250,7 +1335,7 @@ namespace Barotrauma CreateStairBodies(); } - + if (HasBody) { CreateSections(); @@ -1377,7 +1462,7 @@ namespace Barotrauma StructurePrefab prefab = null; if (string.IsNullOrEmpty(identifier)) { - //legacy support: + //legacy support: //1. attempt to find a prefab with an empty identifier and a matching name prefab = MapEntityPrefab.Find(name, "") as StructurePrefab; //2. not found, attempt to find a prefab with a matching name @@ -1422,7 +1507,7 @@ namespace Barotrauma } SerializableProperty.SerializeProperties(this, element); - + foreach (var upgrade in Upgrades) { upgrade.Save(element); @@ -1453,7 +1538,7 @@ namespace Barotrauma { if (aiTarget != null) { - aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; + aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : MathHelper.Lerp(aiTarget.MinSightRange, aiTarget.MaxSightRange, Submarine.Velocity.Length() / 10); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 380df3975..805bc42b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -167,6 +167,9 @@ namespace Barotrauma private set { size = value; } } + [Serialize("", true)] + public string DamageSound { get; private set; } + public Vector2 ScaledSize => size * Scale; protected Vector2 textureScale = Vector2.One; @@ -291,7 +294,8 @@ namespace Barotrauma { case "sprite": sp.sprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null) + if (subElement.Attribute("sourcerect") == null && + subElement.Attribute("sheetindex") == null) { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 2270e5d69..3273bb4c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -249,6 +249,17 @@ namespace Barotrauma get { return subBody?.HullVertices; } } + private int? submarineSpecificIDTag; + public int SubmarineSpecificIDTag + { + get + { + submarineSpecificIDTag ??= ToolBox.StringToInt((Level.Loaded?.Seed ?? "") + Info.Name); + return submarineSpecificIDTag.Value; + } + } + + public bool AtDamageDepth { get @@ -329,48 +340,6 @@ namespace Barotrauma DockedTo.ForEach(s => s.ShowSonarMarker = false); PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = CharacterTeamType.None; - - string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this); - ReplaceIDCardTagRequirements("wreck_id", defaultTag); - - foreach (Item item in Item.ItemList) - { - if (item.Submarine != this) { continue; } - if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") - { - foreach (string tag in item.GetTags().ToList()) - { - if (tag == "smallitem") { continue; } - string newTag = Level.Loaded.GetWreckIDTag(tag, this); - item.ReplaceTag(tag, newTag); - ReplaceIDCardTagRequirements(tag, newTag); - } - } - } - - void ReplaceIDCardTagRequirements(string oldTag, string newTag) - { - foreach (Item item in Item.ItemList) - { - if (item.Submarine != this) { continue; } - foreach (ItemComponent ic in item.Components) - { - ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Picked, oldTag, newTag); - ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Equipped, oldTag, newTag); - } - } - } - - static void ReplaceIDCardTagRequirement(ItemComponent ic, RelatedItem.RelationType relationType, string oldTag, string newTag) - { - if (!ic.requiredItems.ContainsKey(relationType)) { return; } - foreach (RelatedItem requiredItem in ic.requiredItems[relationType]) - { - int index = Array.IndexOf(requiredItem.Identifiers, oldTag); - if (index == -1) { continue; } - requiredItem.Identifiers[index] = newTag; - } - } } public WreckAI WreckAI { get; private set; } @@ -945,9 +914,11 @@ namespace Barotrauma mapEntity.Move(-HiddenSubPosition); } + var prevBodyType = subBody.Body.BodyType; Vector2 pos = new Vector2(subBody.Position.X, subBody.Position.Y); subBody.Body.Remove(); subBody = new SubmarineBody(this); + subBody.Body.BodyType = prevBodyType; SetPosition(pos, new List(parents.Where(p => p != this))); if (entityGrid != null) @@ -1460,6 +1431,11 @@ namespace Barotrauma } } } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + } } if (entityGrid != null) @@ -1718,7 +1694,10 @@ namespace Barotrauma PhysicsBody.RemoveAll(); - GameMain.World.Clear(); + GameMain.World?.Clear(); + GameMain.World = null; + + GC.Collect(); Unloading = false; } @@ -1730,6 +1709,9 @@ namespace Barotrauma subBody?.Remove(); subBody = null; + outdoorNodes?.Clear(); + outdoorNodes = null; + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged -= ResetCrushDepth; @@ -1743,8 +1725,8 @@ namespace Barotrauma visibleEntities = null; - if (MainSub == this) MainSub = null; - if (MainSubs[1] == this) MainSubs[1] = null; + if (MainSub == this) { MainSub = null; } + if (MainSubs[1] == this) { MainSubs[1] = null; } ConnectedDockingPorts?.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 5fe93ad3b..49bfbf5f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -452,7 +452,7 @@ namespace Barotrauma public void ApplyForce(Vector2 force) { - Body.ApplyForce(force, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + Body.ApplyForce(force); } public void SetPosition(Vector2 position) @@ -569,8 +569,7 @@ namespace Barotrauma } var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine); - targetPos = character.WorldPosition; - Gap adjacentGap = Gap.FindAdjacent(gaps, targetPos, 500.0f); + Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f); if (adjacentGap == null) { return true; } if (newHull != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 1a1876939..d8dc6665f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -23,7 +23,7 @@ namespace Barotrauma HideInMenus = 2 } - public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine } + public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine, Ruin } public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver } partial class SubmarineInfo : IDisposable @@ -96,9 +96,11 @@ namespace Barotrauma public OutpostModuleInfo OutpostModuleInfo { get; set; } public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; + public bool IsWreck => Type == SubmarineType.Wreck; public bool IsBeacon => Type == SubmarineType.BeaconStation; public bool IsPlayer => Type == SubmarineType.Player; + public bool IsRuin => Type == SubmarineType.Ruin; public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined; public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 2f7152ffa..de504a73e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.RuinGeneration; using Barotrauma.Extensions; namespace Barotrauma @@ -47,6 +46,7 @@ namespace Barotrauma public Hull CurrentHull { get; private set; } public Level.Tunnel Tunnel; + public RuinGeneration.Ruin Ruin; public SpawnType SpawnType { @@ -54,6 +54,8 @@ namespace Barotrauma set { spawnType = value; } } + public Action OnLinksChanged { get; set; } + public override string Name { get @@ -187,61 +189,141 @@ namespace Barotrauma door.Body.Enabled = true; } } - + bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin"); float diffFromHullEdge = 50; float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; + var removals = new List(); foreach (Hull hull in Hull.hullList) { - // Ignore hulls that a human couldn't fit in. - // Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually. - if (hull.Rect.Height < hullMinHeight) { continue; } - // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor. - Body floor = null; - for (int i = 0; i < 5; i++) + if (isFlooded) { - float horizontalOffset = 0; - switch (i) + diffFromHullEdge = 75; + var hullWaypoints = new List(); + float top = hull.Rect.Y; + float bottom = hull.Rect.Y - hull.Rect.Height; + if (hull.Rect.Width < 300 || hull.Rect.Height < 300) { - case 1: - horizontalOffset = hull.RectWidth * 0.2f; - break; - case 2: - horizontalOffset = hull.RectWidth * 0.4f; - break; - case 3: - horizontalOffset = -hull.RectWidth * 0.2f; - break; - case 4: - horizontalOffset = -hull.RectWidth * 0.4f; - break; + // For narrow hulls, create one line of waypoints either horizontally or vertically + if (hull.Rect.Width > hull.Rect.Height) + { + // Horizontal + float y = hull.Rect.Y - hull.Rect.Height / 2; + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + else + { + // Vertical + float x = hull.Rect.X + hull.Rect.Width / 2; + for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + } + if (hullWaypoints.None()) + { + // Try to create a grid-like network of waypoints + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + if (hullWaypoints.None()) + { + // If that fails, just create one waypoint at the center. + hullWaypoints.Add(new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height / 2), SpawnType.Path, submarine)); + } + foreach (WayPoint wp in hullWaypoints) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.HasBody) + { + // Remove waypoints that are too close/inside the walls. + Rectangle rect = wall.Rect; + rect.Inflate(10, 10); + if (rect.ContainsWorld(wp.Position)) + { + removals.Add(wp); + } + } + } + } + } + // Connect the waypoints + foreach (var wayPoint in hullWaypoints) + { + for (int dir = -1; dir <= 1; dir += 2) + { + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.9f, minDist)); + if (closest != null && closest.CurrentHull == wayPoint.CurrentHull) + { + wayPoint.ConnectTo(closest); + } + closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(minDist, minDist * 1.9f)); + if (closest != null && closest.CurrentHull == wayPoint.CurrentHull) + { + wayPoint.ConnectTo(closest); + } + } } - horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset); - Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); - floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); - if (floor != null) { break; } - } - if (floor == null) { continue; } - float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; - if (hull.Rect.Width < diffFromHullEdge * 3.0f) - { - new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } else { - WayPoint prevWaypoint = null; - for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + if (hull.Rect.Height < hullMinHeight) { continue; } + // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor. + Body floor = null; + for (int i = 0; i < 5; i++) { - var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); - if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); } - prevWaypoint = wayPoint; + float horizontalOffset = 0; + switch (i) + { + case 1: + horizontalOffset = hull.RectWidth * 0.2f; + break; + case 2: + horizontalOffset = hull.RectWidth * 0.4f; + break; + case 3: + horizontalOffset = -hull.RectWidth * 0.2f; + break; + case 4: + horizontalOffset = -hull.RectWidth * 0.4f; + break; + } + horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset); + Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); + floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); + if (floor != null) { break; } } - if (prevWaypoint == null) + if (floor == null) { continue; } + float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; + if (hull.Rect.Width < diffFromHullEdge * 3.0f) { - // Ensure that we always create at least one waypoint per hull. new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } + else + { + WayPoint previousWaypoint = null; + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + if (previousWaypoint != null) { wayPoint.ConnectTo(previousWaypoint); } + previousWaypoint = wayPoint; + } + if (previousWaypoint == null) + { + // Ensure that we always create at least one waypoint per hull. + new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + } + } } } @@ -276,7 +358,7 @@ namespace Barotrauma } float outSideWaypointInterval = 100.0f; - if (submarine.Info.Type != SubmarineType.OutpostModule) + if (!isFlooded && submarine.Info.Type != SubmarineType.OutpostModule) { List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>(); @@ -379,7 +461,6 @@ namespace Barotrauma } } // Remove unwanted points - var removals = new List(); WayPoint previous = null; float tooClose = outSideWaypointInterval / 2; foreach (var wayPoint in outsideWaypoints) @@ -410,7 +491,6 @@ namespace Barotrauma foreach (WayPoint wp in removals) { outsideWaypoints.RemoveAll(w => w.Item1 == wp); - wp.Remove(); } for (int i = 0; i < outsideWaypoints.Count; i++) { @@ -431,41 +511,35 @@ namespace Barotrauma } } } - - List stairList = new List(); - foreach (MapEntity me in mapEntityList) - { - if (!(me is Structure stairs)) { continue; } - - if (stairs.StairDirection != Direction.None) stairList.Add(stairs); - } - - foreach (Structure stairs in stairList) + foreach (Structure wall in Structure.WallList) { + if (wall.StairDirection == Direction.None) { continue; } WayPoint[] stairPoints = new WayPoint[3]; stairPoints[0] = new WayPoint( - new Vector2(stairs.Rect.X - 32.0f, - stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(wall.Rect.X - 32.0f, + wall.Rect.Y - (wall.StairDirection == Direction.Left ? 80 : wall.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); stairPoints[1] = new WayPoint( - new Vector2(stairs.Rect.Right + 32.0f, - stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(wall.Rect.Right + 32.0f, + wall.Rect.Y - (wall.StairDirection == Direction.Left ? wall.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); - for (int i = 0; i < 2; i++ ) + for (int i = 0; i < 2; i++) { for (int dir = -1; dir <= 1; dir += 2) { WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70)); if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); - } + } } - + stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine); stairPoints[0].ConnectTo(stairPoints[2]); stairPoints[2].ConnectTo(stairPoints[1]); } + removals.ForEach(wp => wp.Remove()); + removals.Clear(); foreach (Item item in Item.ItemList) { @@ -603,12 +677,25 @@ namespace Barotrauma { if (gap.IsHorizontal) { - // Too small to walk through - if (gap.Rect.Height < hullMinHeight) { continue; } + if ( isFlooded) + { + // Too small to swim through + if (gap.Rect.Height < 50) { continue; } + } + else + { + // Too small to walk through + if (gap.Rect.Height < hullMinHeight) { continue; } + } + Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor); + if (isFlooded) + { + pos.Y = gap.Rect.Y - gap.Rect.Height / 2; + } var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); // The closest waypoint can be quite far if the gap is at an exterior door. - Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000); + Vector2 tolerance = gap.IsRoomToRoom && !isFlooded ? new Vector2(150, 70) : new Vector2(1000, 1000); for (int dir = -1; dir <= 1; dir += 2) { WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody); @@ -621,7 +708,7 @@ namespace Barotrauma else { // Create waypoints on vertical gaps on the outer walls, also hatches. - if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; } + if (!isFlooded && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; } // Too small to swim through if (gap.Rect.Width < 50.0f) { continue; } Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2); @@ -630,11 +717,20 @@ namespace Barotrauma var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull); int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y); - WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100)); + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100)); if (closest != null) { wayPoint.ConnectTo(closest); } + if (isFlooded) + { + closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100)); + if (closest != null) + { + wayPoint.ConnectTo(closest); + } + } + // Link to outside for (dir = -1; dir <= 1; dir += 2) { closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null); @@ -654,7 +750,7 @@ namespace Barotrauma foreach (WayPoint wp in WayPointList) { - if (wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2) + if (wp.SpawnType == SpawnType.Path && wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2) { DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color."); } @@ -761,17 +857,24 @@ namespace Barotrauma public void ConnectTo(WayPoint wayPoint2) { System.Diagnostics.Debug.Assert(this != wayPoint2); - - if (!linkedTo.Contains(wayPoint2)) { linkedTo.Add(wayPoint2); } - if (!wayPoint2.linkedTo.Contains(this)) { wayPoint2.linkedTo.Add(this); } + if (!linkedTo.Contains(wayPoint2)) + { + OnLinksChanged?.Invoke(this); + linkedTo.Add(wayPoint2); + } + if (!wayPoint2.linkedTo.Contains(this)) + { + wayPoint2.OnLinksChanged?.Invoke(wayPoint2); + wayPoint2.linkedTo.Add(this); + } } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null) { return WayPointList.GetRandom(wp => wp.Submarine == sub && - wp.ParentRuin == ruin && wp.spawnType == spawnType && + (string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); } @@ -986,14 +1089,19 @@ namespace Barotrauma public override void ShallowRemove() { base.ShallowRemove(); - WayPointList.Remove(this); } public override void Remove() { base.Remove(); - + CurrentHull = null; + ConnectedGap = null; + Tunnel = null; + Ruin = null; + Stairs = null; + Ladders = null; + OnLinksChanged = null; WayPointList.Remove(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 7402c316d..d43ef274d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Networking private static int ReadIncomingMsgs() { Task readTask = readStream?.ReadAsync(tempBytes, 0, tempBytes.Length, readCancellationToken.Token); - TimeSpan ts = TimeSpan.FromMilliseconds(100); + TimeSpan timeOut = TimeSpan.FromMilliseconds(100); for (int i = 0; i < 150; i++) { if (shutDown) @@ -99,7 +99,7 @@ namespace Barotrauma.Networking return -1; } - if ((readTask?.IsCompleted ?? true) || (readTask?.Wait(ts) ?? true)) + if ((readTask?.IsCompleted ?? true) || (readTask?.Wait(timeOut) ?? true)) { break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 3d6b8ea44..a4f18118c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -28,6 +29,7 @@ namespace Barotrauma public bool SpawnIfInventoryFull = true; public bool IgnoreLimbSlots = false; + public InvSlotType Slot = InvSlotType.None; private readonly Action onSpawned; @@ -73,7 +75,8 @@ namespace Barotrauma { Condition = Condition }; - if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots)) + var slot = Slot != InvSlotType.None ? Slot.ToEnumerable() : spawnedItem.AllowedSlots; + if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, slot)) { if (IgnoreLimbSlots) { @@ -264,7 +267,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false) + public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -277,7 +280,8 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition) { SpawnIfInventoryFull = spawnIfInventoryFull, - IgnoreLimbSlots = ignoreLimbSlots + IgnoreLimbSlots = ignoreLimbSlots, + Slot = slot }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index f0b800508..8396c48a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -23,6 +23,10 @@ namespace Barotrauma.Networking TeamChange, ObjectiveManagerState, AddToCrew, + UpdateExperience, + UpdateTalents, + UpdateMoney, + UpdatePermanentStats, } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index b6eb1674f..dc79afcd9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -23,9 +23,12 @@ namespace Barotrauma.Networking /// public int? WallSectionIndex { get; set; } + /// + /// Same as calling , but the text parameter is set using + /// public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption, priority: priority), targetEntity, targetCharacter, sender) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index 8a6d8f9e4..f7e5e730c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Networking Double ReadDouble(); UInt32 ReadVariableUInt32(); String ReadString(); + Microsoft.Xna.Framework.Color ReadColorR8G8B8(); + Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(); int ReadRangedInteger(int min, int max); Single ReadRangedSingle(Single min, Single max, int bitCount); byte[] ReadBytes(int numberOfBytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index 653364ae1..16146f8bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Networking void Write(UInt64 val); void Write(Single val); void Write(Double val); + void WriteColorR8G8B8(Microsoft.Xna.Framework.Color val); + void WriteColorR8G8B8A8(Microsoft.Xna.Framework.Color val); void WriteVariableUInt32(UInt32 val); void Write(string val); void WriteRangedInteger(int val, int min, int max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index a84520c56..deb765620 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -5,6 +5,7 @@ using Barotrauma.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -138,6 +139,26 @@ namespace Barotrauma.Networking byte[] bytes = BitConverter.GetBytes(val); WriteBytes(ref buf, ref bitPos, bytes, 0, 8); } + + internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + { + EnsureBufferSize(ref buf, bitPos + 24); + + Write(ref buf, ref bitPos, val.R); + Write(ref buf, ref bitPos, val.G); + Write(ref buf, ref bitPos, val.B); + } + + internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + { + EnsureBufferSize(ref buf, bitPos + 32); + + Write(ref buf, ref bitPos, val.R); + Write(ref buf, ref bitPos, val.G); + Write(ref buf, ref bitPos, val.B); + Write(ref buf, ref bitPos, val.A); + } + internal static void Write(ref byte[] buf, ref int bitPos, string val) { if (string.IsNullOrEmpty(val)) @@ -217,7 +238,7 @@ namespace Barotrauma.Networking { byte retval = NetBitWriter.ReadByte(buf, 1, bitPos); bitPos++; - return (retval > 0 ? true : false); + return retval > 0; } internal static void ReadPadBits(byte[] buf, ref int bitPos) @@ -299,6 +320,23 @@ namespace Barotrauma.Networking return BitConverter.ToDouble(bytes, 0); } + internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8(byte[] buf, ref int bitPos) + { + byte r = ReadByte(buf, ref bitPos); + byte g = ReadByte(buf, ref bitPos); + byte b = ReadByte(buf, ref bitPos); + return new Color(r, g, b, (byte)255); + } + + internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(byte[] buf, ref int bitPos) + { + byte r = ReadByte(buf, ref bitPos); + byte g = ReadByte(buf, ref bitPos); + byte b = ReadByte(buf, ref bitPos); + byte a = ReadByte(buf, ref bitPos); + return new Color(r, g, b, a); + } + internal static UInt32 ReadVariableUInt32(byte[] buf, ref int bitPos) { int bitLength = buf.Length * 8; @@ -482,6 +520,16 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void WriteColorR8G8B8(Color val) + { + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + } + + public void WriteColorR8G8B8A8(Color val) + { + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + } + public void WriteVariableUInt32(UInt32 val) { MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); @@ -624,14 +672,28 @@ namespace Barotrauma.Networking } } buf = new byte[decompressedData.Length]; - Array.Copy(decompressedData, 0, buf, 0, decompressedData.Length); + try + { + Array.Copy(decompressedData, 0, buf, 0, decompressedData.Length); + } + catch (ArgumentException e) + { + throw new ArgumentException($"Failed to copy the incoming compressed buffer. Source buffer length: {decompressedData.Length}, start position: {0}, length: {decompressedData.Length}, destination buffer length: {buf.Length}.", e); + } lengthBits = decompressedData.Length * 8; DebugConsole.Log("Decompressing message: " + inLength + " to " + LengthBytes); } else { buf = new byte[inBuf.Length]; - Array.Copy(inBuf, startPos, buf, 0, inLength); + try + { + Array.Copy(inBuf, startPos, buf, 0, inLength); + } + catch (ArgumentException e) + { + throw new ArgumentException($"Failed to copy the incoming uncompressed buffer. Source buffer length: {inBuf.Length}, start position: {startPos}, length: {inLength}, destination buffer length: {buf.Length}.", e); + } lengthBits = inLength * 8; } seekPos = 0; @@ -702,6 +764,17 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Color ReadColorR8G8B8() + { + return MsgReader.ReadColorR8G8B8(buf, ref seekPos); + } + + public Color ReadColorR8G8B8A8() + { + return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); + } + + public int ReadRangedInteger(int min, int max) { return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); @@ -845,6 +918,16 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void WriteColorR8G8B8(Color val) + { + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + } + + public void WriteColorR8G8B8A8(Color val) + { + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + } + public void WriteVariableUInt32(UInt32 val) { MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); @@ -936,6 +1019,16 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Color ReadColorR8G8B8() + { + return MsgReader.ReadColorR8G8B8(buf, ref seekPos); + } + + public Color ReadColorR8G8B8A8() + { + return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); + } + public int ReadRangedInteger(int min, int max) { return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index a36975fec..f425c0074 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -26,6 +26,9 @@ namespace Barotrauma.Networking //any respawn items left in the shuttle are removed when the shuttle despawns private readonly List respawnItems = new List(); + //characters who spawned during the last respawn + private readonly List respawnedCharacters = new List(); + public bool UsingShuttle { get { return RespawnShuttle != null; } @@ -290,11 +293,17 @@ namespace Barotrauma.Networking hull.BallastFlora?.Kill(); } + Dictionary characterPositions = new Dictionary(); foreach (Character c in Character.CharacterList) { if (c.Submarine != RespawnShuttle) { continue; } + if (!respawnedCharacters.Contains(c)) + { + characterPositions.Add(c, c.WorldPosition); + continue; + } #if CLIENT - if (Character.Controlled == c) Character.Controlled = null; + if (Character.Controlled == c) { Character.Controlled = null; } #endif c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; @@ -311,6 +320,11 @@ namespace Barotrauma.Networking RespawnShuttle.SetPosition(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + RespawnShuttle.Borders.Height)); RespawnShuttle.Velocity = Vector2.Zero; + + foreach (var characterPosition in characterPositions) + { + characterPosition.Key.TeleportTo(characterPosition.Value); + } } partial void RespawnCharactersProjSpecific(Vector2? shuttlePos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 8b72e0075..4d335dc73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Networking } [Serialize(800, true)] - private int LinesPerLogFile + public int LinesPerLogFile { get { @@ -892,8 +892,15 @@ namespace Barotrauma.Networking get; set; } - // we do not serialize this value because it relies on a default setting - public int MaxMissionCount { get; set; } = CampaignSettings.DefaultMaxMissionCount; + + private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; + + [Serialize(CampaignSettings.DefaultMaxMissionCount, true)] + public int MaxMissionCount + { + get { return maxMissionCount; } + set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } + } public void SetPassword(string password) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index d09effa0e..81086e854 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -641,18 +641,9 @@ namespace Barotrauma NetConfig.MaxPhysicsBodyAngularVelocity); } - public void ApplyForce(Vector2 force) + public void ApplyForce(Vector2 force, float maxVelocity = NetConfig.MaxPhysicsBodyVelocity) { - if (!IsValidValue(force, "force", -1e10f, 1e10f)) return; - FarseerBody.ApplyForce(force); - } - - /// - /// Apply a force to the body without increasing it's velocity above a specific limit. - /// - public void ApplyForce(Vector2 force, float maxVelocity) - { - if (!IsValidValue(maxVelocity, "max velocity")) return; + if (!IsValidValue(maxVelocity, "max velocity")) { return; } Vector2 velocityAddition = force / Mass * (float)Timing.Step; Vector2 newVelocity = FarseerBody.LinearVelocity + velocityAddition; @@ -666,20 +657,20 @@ namespace Barotrauma force = velocityAddition.ClampLength(maxVelAddition) * Mass / (float)Timing.Step; } - if (!IsValidValue(force, "clamped force", -1e10f, 1e10f)) return; + if (!IsValidValue(force, "clamped force", -1e10f, 1e10f)) { return; } FarseerBody.ApplyForce(force); } public void ApplyForce(Vector2 force, Vector2 point) { - if (!IsValidValue(force, "force", -1e10f, 1e10f)) return; - if (!IsValidValue(point, "point")) return; + if (!IsValidValue(force, "force", -1e10f, 1e10f)) { return; } + if (!IsValidValue(point, "point")) { return; } FarseerBody.ApplyForce(force, point); } public void ApplyTorque(float torque) { - if (!IsValidValue(torque, "torque")) return; + if (!IsValidValue(torque, "torque")) { return; } FarseerBody.ApplyTorque(torque); } @@ -689,8 +680,8 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f); - if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return false; - if (!IsValidValue(rotation, "rotation")) return false; + if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } + if (!IsValidValue(rotation, "rotation")) { return false; } FarseerBody.SetTransform(simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } @@ -703,8 +694,8 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Math.Abs(simPosition.X) < 1000000.0f); System.Diagnostics.Debug.Assert(Math.Abs(simPosition.Y) < 1000000.0f); - if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return false; - if (!IsValidValue(rotation, "rotation")) return false; + if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return false; } + if (!IsValidValue(rotation, "rotation")) { return false; } FarseerBody.SetTransformIgnoreContacts(ref simPosition, rotation); if (setPrevTransform) { SetPrevTransform(simPosition, rotation); } @@ -756,12 +747,13 @@ namespace Barotrauma Vector2 vel = FarseerBody.LinearVelocity; Vector2 deltaPos = simPosition - (Vector2)pullPos; -#if DEBUG if (deltaPos.LengthSquared() > 100.0f * 100.0f) { +#if DEBUG DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace()); - } #endif + return; + } deltaPos *= force; ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos); } @@ -788,7 +780,7 @@ namespace Barotrauma dragForce = Math.Min(drag, Mass * 500.0f) * -velDir; } - ApplyForce(dragForce + buoyancy, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + ApplyForce(dragForce + buoyancy); ApplyTorque(FarseerBody.AngularVelocity * FarseerBody.Mass * -0.08f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 8cd8dae34..f8ce3190b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -73,19 +73,7 @@ namespace Barotrauma public int GetMaxMissionCount() { -#if CLIENT - // this seems rather silly, but it matches the radiation enabled check structurally. is this right? - if (maxMissionCountText != null && Int32.TryParse(maxMissionCountText.Text, out int result)) - { - return result; - } - else - { - return 0; - } -#elif SERVER - return GameMain.Server.ServerSettings.MaxMissionCount; -#endif + return GameMain.NetworkMember?.ServerSettings?.MaxMissionCount ?? 0; } public void ToggleTraitorsEnabled(int dir) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 5002ddf18..1f4ff6489 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -88,11 +88,11 @@ namespace Barotrauma return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; case ConditionType.IsSwappableItem: { - return entity is Item item && item.Prefab.SwappableItem != null; + return entity is Item item && item.Prefab.SwappableItem != null && Screen.Selected == GameMain.SubEditorScreen; } case ConditionType.AllowRotating: { - return entity is Item item && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen; + return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen; } } return false; @@ -146,6 +146,7 @@ namespace Barotrauma { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, + { typeof(string[]), "stringarray" } }; private static readonly Dictionary> cachedProperties = @@ -273,6 +274,9 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true)); break; + case "stringarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); + break; } } @@ -345,6 +349,9 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false)); return true; + case "stringarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); + break; default: DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); @@ -723,6 +730,10 @@ namespace Barotrauma case "rectangle": stringValue = XMLExtensions.RectToString((Rectangle)value); break; + case "stringarray": + string[] stringArray = (string[])value; + stringValue = stringArray != null ? string.Join(';', stringArray) : ""; + break; default: stringValue = value.ToString(); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 0e6da820c..c66595a6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using System.Xml; @@ -8,21 +10,42 @@ using System.Xml.Linq; using Microsoft.Xna.Framework; using File = Barotrauma.IO.File; using FileStream = Barotrauma.IO.FileStream; +using Path = Barotrauma.IO.Path; namespace Barotrauma { public static class XMLExtensions { - public static string ParseContentPathFromUri(this XObject element) => ToolBox.ConvertAbsoluteToRelativePath(element.BaseUri); + private static ImmutableDictionary> converters + = new Dictionary>() + { + { typeof(string), (str, defVal) => str }, + { typeof(int), (str, defVal) => int.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out int result) ? result : defVal }, + { typeof(uint), (str, defVal) => uint.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out uint result) ? result : defVal }, + { typeof(UInt64), (str, defVal) => UInt64.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out UInt64 result) ? result : defVal }, + { typeof(float), (str, defVal) => float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out float result) ? result : defVal }, + { typeof(bool), (str, defVal) => bool.TryParse(str, out bool result) ? result : defVal }, + { typeof(Color), (str, defVal) => ParseColor(str) }, + { typeof(Vector2), (str, defVal) => ParseVector2(str) }, + { typeof(Vector3), (str, defVal) => ParseVector3(str) }, + { typeof(Vector4), (str, defVal) => ParseVector4(str) }, + { typeof(Rectangle), (str, defVal) => ParseRect(str, true) } + }.ToImmutableDictionary(); + + public static string ParseContentPathFromUri(this XObject element) + => !string.IsNullOrWhiteSpace(element.BaseUri) + ? System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri.CleanUpPath()) + : ""; public static readonly XmlReaderSettings ReaderSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, - XmlResolver = null + XmlResolver = null, + IgnoreWhitespace = true, }; - - public static XmlReader CreateReader(System.IO.Stream stream) - => XmlReader.Create(stream, ReaderSettings); + + public static XmlReader CreateReader(System.IO.Stream stream, string baseUri = "") + => XmlReader.Create(stream, ReaderSettings, baseUri); public static XDocument TryLoadXml(System.IO.Stream stream) { @@ -52,8 +75,8 @@ namespace Barotrauma { ToolBox.IsProperFilenameCase(filePath); using FileStream stream = File.Open(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using XmlReader reader = CreateReader(stream); - doc = XDocument.Load(reader); + using XmlReader reader = CreateReader(stream, Path.GetFullPath(filePath)); + doc = XDocument.Load(reader, LoadOptions.SetBaseUri); } catch (Exception e) { @@ -79,7 +102,7 @@ namespace Barotrauma try { using FileStream stream = File.Open(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using XmlReader reader = CreateReader(stream); + using XmlReader reader = CreateReader(stream, Path.GetFullPath(filePath)); doc = XDocument.Load(reader); } catch @@ -87,7 +110,7 @@ namespace Barotrauma return null; } - if (doc.Root == null) return null; + if (doc.Root == null) { return null; } } return doc; @@ -95,20 +118,18 @@ namespace Barotrauma public static object GetAttributeObject(XAttribute attribute) { - if (attribute == null) return null; + if (attribute == null) { return null; } return ParseToObject(attribute.Value.ToString()); } public static object ParseToObject(string value) { - float floatVal; - int intVal; - if (value.Contains(".") && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out floatVal)) + if (value.Contains(".") && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { return floatVal; } - if (Int32.TryParse(value, out intVal)) + if (Int32.TryParse(value, out int intVal)) { return intVal; } @@ -129,7 +150,7 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return GetAttributeString(element.Attribute(name), defaultValue); } @@ -141,10 +162,10 @@ namespace Barotrauma public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); @@ -168,11 +189,11 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { - if (element == null) return defaultValue; + if (element == null) { return defaultValue; } foreach (string name in matchingAttributeName) { - if (element.Attribute(name) == null) continue; + if (element.Attribute(name) == null) { continue; } float val; try @@ -197,7 +218,7 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, string name, float defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } float val = defaultValue; try @@ -219,7 +240,7 @@ namespace Barotrauma public static float GetAttributeFloat(this XAttribute attribute, float defaultValue) { - if (attribute == null) return defaultValue; + if (attribute == null) { return defaultValue; } float val = defaultValue; @@ -242,10 +263,10 @@ namespace Barotrauma public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); float[] floatValue = new float[splitValue.Length]; @@ -271,13 +292,16 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } int val = defaultValue; try { - val = Int32.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + if (!Int32.TryParse(element.Attribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + { + val = (int)float.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + } } catch (Exception e) { @@ -289,7 +313,7 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } uint val = defaultValue; @@ -307,7 +331,7 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; @@ -325,7 +349,7 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; @@ -343,10 +367,10 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); int[] intValue = new int[splitValue.Length]; @@ -367,10 +391,10 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); ushort[] ushortValue = new ushort[splitValue.Length]; @@ -392,13 +416,13 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return element.Attribute(name).GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) { - if (attribute == null) return defaultValue; + if (attribute == null) { return defaultValue; } string val = attribute.Value.ToLowerInvariant().Trim(); if (val == "true") @@ -416,31 +440,31 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return ParsePoint(element.Attribute(name).Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } return ParseVector2(element.Attribute(name).Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseVector3(element.Attribute(name).Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseVector4(element.Attribute(name).Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseColor(element.Attribute(name).Value); } @@ -452,35 +476,54 @@ namespace Barotrauma public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.Attribute(name) == null) return defaultValue; + if (element?.Attribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; - if (string.IsNullOrEmpty(stringValue)) return defaultValue; + string stringValue = element.Attribute(name).Value; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } - string[] splitValue = stringValue.Split(';'); - Color[] colorValue = new Color[splitValue.Length]; - for (int i = 0; i < splitValue.Length; i++) + string[] splitValue = stringValue.Split(';'); + Color[] colorValue = new Color[splitValue.Length]; + for (int i = 0; i < splitValue.Length; i++) + { + try { - try - { - Color val = ParseColor(splitValue[i], true); - colorValue[i] = val; - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "! ", e); - } + Color val = ParseColor(splitValue[i], true); + colorValue[i] = val; } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + } - return colorValue; + return colorValue; } public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.Attribute(name) == null) return defaultValue; + if (element == null || element.Attribute(name) == null) { return defaultValue; } return ParseRect(element.Attribute(name).Value, false); } + //TODO: nested tuples and and n-uples where n!=2 are unsupported + public static (T1, T2) GetAttributeTuple(this XElement element, string name, (T1, T2) defaultValue) + { + string strValue = element.GetAttributeString(name, $"({defaultValue.Item1}, {defaultValue.Item2})").Trim(); + + return ParseTuple(strValue, defaultValue); + } + + public static (T1, T2)[] GetAttributeTupleArray(this XElement element, string name, + (T1, T2)[] defaultValue) + { + if (element?.Attribute(name) == null) { return defaultValue; } + + string stringValue = element.Attribute(name).Value; + if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } + + return stringValue.Split(';').Select(s => ParseTuple(s, default)).ToArray(); + } + public static string ElementInnerText(this XElement el) { StringBuilder str = new StringBuilder(); @@ -516,15 +559,33 @@ namespace Barotrauma vector.W.ToString(format, CultureInfo.InvariantCulture); } + [Obsolete("Prefer XMLExtensions.ToStringHex")] public static string ColorToString(Color color) - { - return color.R + "," + color.G + "," + color.B + "," + color.A; - } + => $"{color.R},{color.G},{color.B},{color.A}"; + + public static string ToStringHex(this Color color) + => $"#{color.R:X2}{color.G:X2}{color.B:X2}" + + ((color.A < 255) ? $"{color.A:X2}" : ""); public static string RectToString(Rectangle rect) { return rect.X + "," + rect.Y + "," + rect.Width + "," + rect.Height; } + + public static (T1, T2) ParseTuple(string strValue, (T1, T2) defaultValue) + { + strValue = strValue.Trim(); + //require parentheses + if (strValue[0] != '(' || strValue[^1] != ')') { return defaultValue; } + //remove parentheses + strValue = strValue[1..^1]; + + string[] elems = strValue.Split(','); + if (elems.Length != 2) { return defaultValue; } + + return ((T1)converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1), + (T2)converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2)); + } public static Point ParsePoint(string stringPoint, bool errorMessages = true) { @@ -533,7 +594,7 @@ namespace Barotrauma if (components.Length != 2) { - if (!errorMessages) return point; + if (!errorMessages) { return point; } DebugConsole.ThrowError("Failed to parse the string \"" + stringPoint + "\" to Vector2"); return point; } @@ -551,7 +612,7 @@ namespace Barotrauma if (components.Length != 2) { - if (!errorMessages) return vector; + if (!errorMessages) { return vector; } DebugConsole.ThrowError("Failed to parse the string \"" + stringVector2 + "\" to Vector2"); return vector; } @@ -570,7 +631,7 @@ namespace Barotrauma if (components.Length != 3) { - if (!errorMessages) return vector; + if (!errorMessages) { return vector; } DebugConsole.ThrowError("Failed to parse the string \"" + stringVector3 + "\" to Vector3"); return vector; } @@ -590,7 +651,7 @@ namespace Barotrauma if (components.Length < 3) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringVector4 + "\" to Vector4"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringVector4 + "\" to Vector4"); } return vector; } @@ -598,7 +659,9 @@ namespace Barotrauma Single.TryParse(components[1], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Y); Single.TryParse(components[2], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Z); if (components.Length > 3) + { Single.TryParse(components[3], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.W); + } return vector; } @@ -638,8 +701,7 @@ namespace Barotrauma { stringColor = stringColor.Substring(1); - int colorInt = 0; - if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out colorInt)) + if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int colorInt)) { if (stringColor.Length == 6) { @@ -656,7 +718,7 @@ namespace Barotrauma if (hexFailed) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); } return Color.White; } } @@ -686,7 +748,7 @@ namespace Barotrauma string[] strComponents = stringRect.Split(','); if ((strComponents.Length < 3 && requireSize) || strComponents.Length < 2) { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringRect + "\" to Rectangle"); + if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringRect + "\" to Rectangle"); } return new Rectangle(0, 0, 0, 0); } @@ -713,6 +775,11 @@ namespace Barotrauma return floatArray; } + public static string[] ParseStringArray(string stringArrayValues) + { + return string.IsNullOrEmpty(stringArrayValues) ? new string[0] : stringArrayValues.Split(';'); + } + public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 6c0eccc0e..db53a6db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -151,6 +151,12 @@ namespace Barotrauma { sourceVector = overrideElement.GetAttributeVector4("sourcerect", Vector4.Zero); } + if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null) + { + Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero); + Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero); + sourceVector = new Vector4(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y); + } Compress = SourceElement.GetAttributeBool("compress", true); bool shouldReturn = false; if (!lazyLoad) @@ -236,7 +242,7 @@ namespace Barotrauma { lock (list) { - list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s==this); + list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == this); } DisposeTexture(); } @@ -294,6 +300,12 @@ namespace Barotrauma { sourceRect = overrideElement.GetAttributeRect("sourcerect", Rectangle.Empty); } + if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null) + { + Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero); + Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero); + sourceRect = new Rectangle(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y); + } size = SourceElement.GetAttributeVector2("size", Vector2.One); size.X *= sourceRect.Width; size.Y *= sourceRect.Height; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 6925b0dac..258bd5466 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -60,6 +60,8 @@ namespace Barotrauma // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. public readonly bool TargetGrandParent; + public readonly bool TargetContainedItem; + // Remove this after refactoring public static bool IsValid(XAttribute attribute) { @@ -112,6 +114,7 @@ namespace Barotrauma TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); + TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); if (!Enum.TryParse(AttributeName, true, out Type)) { @@ -171,6 +174,22 @@ namespace Barotrauma public bool Matches(ISerializableEntity target) { + if (TargetContainedItem) + { + if (target is Item item) + { + return item.ContainedItems.Any(it => Matches(it)); + } + else if (target is Items.Components.ItemComponent ic) + { + return ic.Item.ContainedItems.Any(it => Matches(it)); + } + else if (target is Character character) + { + return character.Inventory != null && character.Inventory.AllItems.Any(it => Matches(it)); + } + } + switch (Type) { case ConditionType.PropertyValue: diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 41c47a0b7..6bb5586c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -137,12 +137,14 @@ namespace Barotrauma public readonly ItemPrefab ItemPrefab; public readonly SpawnPositionType SpawnPosition; + public readonly bool SpawnIfInventoryFull; public readonly float Speed; public readonly float Rotation; public readonly int Count; public readonly float Spread; public readonly SpawnRotationType RotationType; public readonly float AimSpread; + public readonly bool Equip; public ItemSpawnInfo(XElement element, string parentDebugName) { @@ -173,12 +175,14 @@ namespace Barotrauma } } + SpawnIfInventoryFull = element.GetAttributeBool("spawnifinventoryfull", false); Speed = element.GetAttributeFloat("speed", 0.0f); Rotation = element.GetAttributeFloat("rotation", 0.0f); Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); AimSpread = element.GetAttributeFloat("aimspread", 0f); + Equip = element.GetAttributeBool("equip", false); string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) @@ -193,6 +197,18 @@ namespace Barotrauma } } + public class GiveTalentInfo + { + public string[] TalentIdentifiers; + public bool GiveRandom; + + public GiveTalentInfo(XElement element, string parentDebugName) + { + TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true); + GiveRandom = element.GetAttributeBool("giverandom", false); + } + } + public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; @@ -223,6 +239,11 @@ namespace Barotrauma private readonly TargetType targetTypes; protected HashSet targetIdentifiers; + /// + /// Index of the slot the target must be in when targeting a Contained item + /// + public int TargetSlot = -1; + private readonly List requiredItems; public readonly string[] propertyNames; @@ -262,7 +283,11 @@ namespace Barotrauma public readonly List Explosions; private readonly List spawnItems; + private readonly bool spawnItemRandomly; private readonly List spawnCharacters; + + public readonly List giveTalentInfos; + private readonly List aiTriggers; private readonly List triggeredEvents; @@ -294,7 +319,12 @@ namespace Barotrauma get { return targetIdentifiers; } } - public HashSet AllowedAfflictions { get; private set; } + /// + /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. + /// + private readonly HashSet<(string affliction, float strength)> requiredAfflictions; + + public float AfflictionMultiplier = 1.0f; public List Afflictions { @@ -302,12 +332,17 @@ namespace Barotrauma private set; } + private readonly bool modifyAfflictionsByMaxVitality; + public IEnumerable SpawnCharacters { get { return spawnCharacters; } } - public readonly List> ReduceAffliction; + public readonly List<(string affliction, float amount)> ReduceAffliction; + + private readonly List giveExperiences; + private readonly List<(string identifier, float amount)> giveSkills; public float Duration => duration; @@ -351,18 +386,26 @@ namespace Barotrauma { requiredItems = new List(); spawnItems = new List(); + spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); spawnCharacters = new List(); + giveTalentInfos = new List(); aiTriggers = new List(); Afflictions = new List(); Explosions = new List(); triggeredEvents = new List(); - ReduceAffliction = new List>(); + ReduceAffliction = new List<(string affliction, float amount)>(); + giveExperiences = new List(); + giveSkills = new List<(string, float)>(); + modifyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); + tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); OnlyInside = element.GetAttributeBool("onlyinside", false); OnlyOutside = element.GetAttributeBool("onlyoutside", false); OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); + TargetSlot = element.GetAttributeInt("targetslot", -1); + Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); @@ -430,11 +473,12 @@ namespace Barotrauma } break; case "allowedafflictions": + case "requiredafflictions": string[] types = attribute.Value.Split(','); - AllowedAfflictions = new HashSet(); + requiredAfflictions ??= new HashSet<(string, float)>(); for (int i = 0; i < types.Length; i++) { - AllowedAfflictions.Add(types[i].Trim().ToLowerInvariant()); + requiredAfflictions.Add((types[i].Trim().ToLowerInvariant(), 0.0f)); } break; case "duration": @@ -486,13 +530,22 @@ namespace Barotrauma } } + if (duration > 0.0f && !setValue) + { + //a workaround to "tags" possibly meaning either an item's tags or this status effect's tags: + //if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched. + propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase)); + } + int count = propertyAttributes.Count; + propertyNames = new string[count]; propertyEffects = new object[count]; int n = 0; foreach (XAttribute attribute in propertyAttributes) { + propertyNames[n] = attribute.Name.ToString().ToLowerInvariant(); propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); n++; @@ -536,6 +589,16 @@ namespace Barotrauma } requiredItems.Add(newRequiredItem); break; + case "requiredaffliction": + requiredAfflictions ??= new HashSet<(string, float)>(); + string[] ids = subElement.GetAttributeStringArray("identifier", null) ?? subElement.GetAttributeStringArray("type", new string[0]); + foreach (string afflictionId in ids) + { + requiredAfflictions.Add(( + afflictionId, + subElement.GetAttributeFloat("minstrength", 0.0f))); + } + break; case "conditional": foreach (XAttribute attribute in subElement.Attributes()) { @@ -578,7 +641,7 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); - ReduceAffliction.Add(new Pair( + ReduceAffliction.Add(( subElement.GetAttributeString("name", "").ToLowerInvariant(), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -589,9 +652,7 @@ namespace Barotrauma if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { - ReduceAffliction.Add(new Pair( - name, - subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); + ReduceAffliction.Add((name, subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } else { @@ -623,9 +684,19 @@ namespace Barotrauma var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } break; + case "givetalentinfo": + var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName); + if (newGiveTalentInfo.TalentIdentifiers.Any()) { giveTalentInfos.Add(newGiveTalentInfo); } + break; case "aitrigger": aiTriggers.Add(new AITrigger(subElement)); break; + case "giveexperience": + giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); + break; + case "giveskill": + giveSkills.Add((subElement.GetAttributeString("skillidentifier", ""), subElement.GetAttributeFloat("amount", 0))); + break; } } InitProjSpecific(element, parentDebugName); @@ -651,6 +722,17 @@ namespace Barotrauma return false; } + public bool HasRequiredAfflictions(AttackResult attackResult) + { + if (requiredAfflictions == null) { return true; } + if (attackResult.Afflictions == null) { return false; } + if (attackResult.Afflictions.None(a => requiredAfflictions.Any(a2 => a.Strength >= a2.strength && (a.Identifier == a2.affliction || a.Prefab.AfflictionType == a2.affliction)))) + { + return false; + } + return true; + } + public virtual bool HasRequiredItems(Entity entity) { if (entity == null) { return true; } @@ -742,7 +824,16 @@ namespace Barotrauma { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) { continue; } + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + return true; + } + continue; + } var owner = targetItem.ParentInventory.Owner; if (pc.TargetGrandParent && owner is Item ownerItem) { @@ -760,7 +851,7 @@ namespace Barotrauma if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } } } - if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } } else { @@ -785,7 +876,16 @@ namespace Barotrauma { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) { return false; } + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + continue; + } + return false; + } var owner = targetItem.ParentInventory.Owner; if (pc.TargetGrandParent && owner is Item ownerItem) { @@ -1097,13 +1197,10 @@ namespace Barotrauma { if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction newAffliction = affliction; - if (!disableDeltaTime && !setValue) - { - newAffliction = affliction.CreateMultiplied(deltaTime); - } if (target is Character character) { if (character.Removed) { continue; } + newAffliction = GetMultipliedAffliction(affliction, entity, character, deltaTime, modifyAfflictionsByMaxVitality); character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { @@ -1112,6 +1209,7 @@ namespace Barotrauma if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + RegisterTreatmentResults(entity, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } @@ -1120,14 +1218,15 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } + newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, modifyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + RegisterTreatmentResults(entity, limb, affliction, result); } } - foreach (Pair reduceAffliction in ReduceAffliction) + foreach (var (affliction, amount) in ReduceAffliction) { - float reduceAmount = disableDeltaTime || setValue ? reduceAffliction.Second : reduceAffliction.Second * deltaTime; Limb targetLimb = null; Character targetCharacter = null; if (target is Character character) @@ -1141,8 +1240,11 @@ namespace Barotrauma } if (targetCharacter != null && !targetCharacter.Removed) { + ActionType? actionType = null; + if (entity is Item item && item.UseInHealthInterface) { actionType = type; } + float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAmount); + targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); if (user != null && user != targetCharacter) { if (!targetCharacter.IsDead) @@ -1181,6 +1283,71 @@ namespace Barotrauma } } } + + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + // these effects do not need to be run clientside, as they are replicated from server to clients anyway + + foreach (int giveExperience in giveExperiences) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + targetCharacter?.Info?.GiveExperience(giveExperience); + } + } + + if (giveSkills.Any()) + { + foreach ((string skillIdentifier, float amount) in giveSkills) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter != null && !targetCharacter.Removed) + { + if (skillIdentifier?.ToLowerInvariant() == "randomskill") + { + targetCharacter.Info?.IncreaseSkillLevel(GetRandomSkill(), amount); + + string GetRandomSkill() + { + return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); + } + } + else + { + targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier?.ToLowerInvariant(), amount); + } + } + } + } + + if (giveTalentInfos.Any()) + { + Character targetCharacter = CharacterFromTarget(target); + if (targetCharacter?.Info == null) { continue; } + if (!TalentTree.JobTalentTrees.TryGetValue(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } + // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well + IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); + + foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos) + { + IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); + if (viableTalents.None()) { continue; } + + if (giveTalentInfo.GiveRandom) + { + targetCharacter.GiveTalent(viableTalents.GetRandom(), true); + } + else + { + foreach (string talent in viableTalents) + { + targetCharacter.GiveTalent(talent, true); + } + } + } + } + } } if (FireSize > 0.0f && entity != null) @@ -1243,109 +1410,148 @@ namespace Barotrauma }); } } - foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) - { - for (int i = 0; i < itemSpawnInfo.Count; i++) - { - switch (itemSpawnInfo.SpawnPosition) - { - case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position + Rand.Vector(itemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => - { - Projectile projectile = newItem.GetComponent(); - if (projectile != null && user != null && sourceBody != null && entity != null) - { - var rope = newItem.GetComponent(); - if (rope != null && sourceBody.UserData is Limb sourceLimb) - { - rope.Attach(sourceLimb, newItem); - } - float spread = MathHelper.ToRadians(Rand.Range(-itemSpawnInfo.AimSpread, itemSpawnInfo.AimSpread)); - var worldPos = sourceBody.Position; - float rotation = itemSpawnInfo.Rotation; - if (user.Submarine != null) - { - worldPos += user.Submarine.Position; - } - switch (itemSpawnInfo.RotationType) - { - case ItemSpawnInfo.SpawnRotationType.Fixed: - rotation = sourceBody.TransformRotation(itemSpawnInfo.Rotation); - break; - case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); - break; - case ItemSpawnInfo.SpawnRotationType.Limb: - rotation = sourceBody.TransformedRotation; - break; - case ItemSpawnInfo.SpawnRotationType.Collider: - rotation = user.AnimController.Collider.Rotation; - break; - case ItemSpawnInfo.SpawnRotationType.MainLimb: - rotation = user.AnimController.MainLimb.body.TransformedRotation; - break; - default: - throw new NotImplementedException("Not implemented: " + itemSpawnInfo.RotationType); - } - rotation += MathHelper.ToRadians(itemSpawnInfo.Rotation * user.AnimController.Dir); - projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); - } - else - { - newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * itemSpawnInfo.Speed); - newItem.Rotation = itemSpawnInfo.Rotation; - } - }); - 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) - { - inventory = item?.GetComponent()?.Inventory; - } - if (inventory != null && inventory.CanBePut(itemSpawnInfo.ItemPrefab)) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: false); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.ContainedInventory: - { - Inventory thisInventory = null; - if (entity is Character character) - { - thisInventory = character.Inventory; - } - else if (entity is Item item) - { - thisInventory = item?.GetComponent()?.Inventory; - } - if (thisInventory != null) - { - foreach (Item item in thisInventory.AllItems) - { - Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory != null && containedInventory.CanBePut(itemSpawnInfo.ItemPrefab)) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: false); - } - break; - } - } - } - break; + if (spawnItemRandomly) + { + SpawnItem(spawnItems.GetRandom()); + } + else + { + foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) + { + for (int i = 0; i < itemSpawnInfo.Count; i++) + { + SpawnItem(itemSpawnInfo); } } } + + + void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) + { + switch (chosenItemSpawnInfo.SpawnPosition) + { + case ItemSpawnInfo.SpawnPositionType.This: + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => + { + Projectile projectile = newItem.GetComponent(); + if (projectile != null && user != null && sourceBody != null && entity != null) + { + var rope = newItem.GetComponent(); + if (rope != null && sourceBody.UserData is Limb sourceLimb) + { + rope.Attach(sourceLimb, newItem); + } + + float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread)); + var worldPos = sourceBody.Position; + float rotation = chosenItemSpawnInfo.Rotation; + if (user.Submarine != null) + { + worldPos += user.Submarine.Position; + } + switch (chosenItemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.Fixed: + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation); + break; + case ItemSpawnInfo.SpawnRotationType.Target: + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + rotation = sourceBody.TransformedRotation; + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + rotation = user.AnimController.Collider.Rotation; + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + rotation = user.AnimController.MainLimb.body.TransformedRotation; + break; + default: + throw new NotImplementedException("Not implemented: " + chosenItemSpawnInfo.RotationType); + } + rotation += MathHelper.ToRadians(chosenItemSpawnInfo.Rotation * user.AnimController.Dir); + projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + } + else + { + newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + newItem.Rotation = chosenItemSpawnInfo.Rotation; + } + }); + 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) + { + inventory = item?.GetComponent()?.Inventory; + } + if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) + { + Entity.Spawner.AddToSpawnQueue(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); + } + }); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.ContainedInventory: + { + Inventory thisInventory = null; + if (entity is Character character) + { + thisInventory = character.Inventory; + } + else if (entity is Item item) + { + thisInventory = item?.GetComponent()?.Inventory; + } + 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.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull); + } + break; + } + } + } + break; + } + } } ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + + static Character CharacterFromTarget(ISerializableEntity target) + { + Character targetCharacter = target as Character; + if (targetCharacter == null) + { + if (target is Limb targetLimb && !targetLimb.Removed) + { + targetCharacter = targetLimb.character; + } + } + return targetCharacter; + } } partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition, bool playSound); @@ -1353,38 +1559,31 @@ namespace Barotrauma private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - Type type = value.GetType(); - if (type == typeof(float) || (type == typeof(int) && property.GetValue(target) is float)) + if (value is int || value is float) { - float floatValue = Convert.ToSingle(value) * deltaTime; - if (!setValue) + var propertyValue = property.GetValue(target); + if (propertyValue is float propertyValueF) { - floatValue += (float)property.GetValue(target); + float floatValue = Convert.ToSingle(value) * deltaTime; + if (!setValue) + { + floatValue += propertyValueF; + } + property.TrySetValue(target, floatValue); + return; } - property.TrySetValue(target, floatValue); - } - else if (type == typeof(int) && value is int) - { - int intValue = (int)((int)value * deltaTime); - if (!setValue) + else if (propertyValue is int integer) { - intValue += (int)property.GetValue(target); + int intValue = (int)(Convert.ToInt32(value) * deltaTime); + if (!setValue) + { + intValue += integer; + } + property.TrySetValue(target, intValue); + return; } - property.TrySetValue(target, intValue); - } - else if (type == typeof(bool) && value is bool) - { - property.TrySetValue(target, (bool)value); - } - else if (type == typeof(string)) - { - property.TrySetValue(target, (string)value); - } - else - { - DebugConsole.ThrowError("Couldn't apply value " + value.ToString() + " (" + type + ") to property \"" + property.Name + "\" (" + property.GetValue(target).GetType() + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); } + property.TrySetValue(target, value); } public static void UpdateAll(float deltaTime) @@ -1426,22 +1625,24 @@ namespace Barotrauma foreach (Affliction affliction in element.Parent.Afflictions) { - Affliction multipliedAffliction = affliction; - if (!element.Parent.disableDeltaTime && !element.Parent.setValue) { multipliedAffliction = affliction.CreateMultiplied(deltaTime); } - + Affliction newAffliction = affliction; if (target is Character character) { if (character.Removed) { continue; } - character.AddDamage(character.WorldPosition, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.modifyAfflictionsByMaxVitality); + var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); + element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(limb.WorldPosition, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.modifyAfflictionsByMaxVitality); + var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); } } - foreach (Pair reduceAffliction in element.Parent.ReduceAffliction) + foreach (var (affliction, amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -1456,8 +1657,11 @@ namespace Barotrauma } if (targetCharacter != null && !targetCharacter.Removed) { + ActionType? actionType = null; + if (element.Entity is Item item && item.UseInHealthInterface) { actionType = element.Parent.type; } + float reduceAmount = amount * element.Parent.GetAfflictionMultiplier(element.Entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAffliction.Second * deltaTime); + targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); if (element.User != null && element.User != targetCharacter) { if (!targetCharacter.IsDead) @@ -1486,6 +1690,53 @@ namespace Barotrauma } } + private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) + { + float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem && sourceItem.HasTag("medical")) + { + multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + } + return multiplier * AfflictionMultiplier; + } + + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality) + { + float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); + if (modifyByMaxVitality) + { + afflictionMultiplier *= targetCharacter.MaxVitality / 100f; + } + + if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) + { + return affliction.CreateMultiplied(afflictionMultiplier); + } + return affliction; + } + + private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result) + { + if (entity is Item item && item.UseInHealthInterface && limb != null) + { + foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) + { + if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) + { + if (type == ActionType.OnUse) + { + limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + } + else if (type == ActionType.OnFailure) + { + limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + } + } + } + } + } + static partial void UpdateAllProjSpecific(float deltaTime); public static void StopAll() diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 24c3db01d..f05045e14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -49,7 +49,7 @@ namespace Barotrauma Reactor reactor = item.GetComponent(); if (reactor != null) { roundData.Reactors.Add(reactor); } } - pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + pathFinder = new PathFinder(WayPoint.WayPointList, false); cachedDistances.Clear(); } @@ -192,7 +192,7 @@ namespace Barotrauma static CachedDistance CalculateNewCachedDistance(Character c) { - pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + pathFinder ??= new PathFinder(WayPoint.WayPointList, false); var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition)); if (path.Unreachable) { return null; } return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f)); @@ -455,6 +455,8 @@ namespace Barotrauma UnlockAchievement(character, character.Info.Job.Prefab.Identifier + "round"); } } + + pathFinder = null; } private static void UnlockAchievement(Character recipient, string identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index c7aa407ea..c19d2db20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Barotrauma.Extensions; +using System.Xml.Linq; namespace Barotrauma { @@ -443,6 +444,60 @@ namespace Barotrauma } } + /// + /// Constructs a string from XML in a way that allows replacing one or more variables with hard-coded or localized values. Usage example in the method's comments. + /// + public static void ConstructDescription(ref string Description, XElement descriptionElement) + { + /* + + + + + + */ + + if (descriptionElement.GetAttributeBool("linebreak", false)) + { + Description += "\n"; + return; + } + + string descriptionTag = descriptionElement.GetAttributeString("tag", string.Empty); + string extraDescriptionLine = Get(descriptionTag); + if (string.IsNullOrEmpty(extraDescriptionLine)) { return; } + foreach (XElement replaceElement in descriptionElement.Elements()) + { + if (replaceElement.Name.ToString().ToLowerInvariant() != "replace") { continue; } + + string tag = replaceElement.GetAttributeString("tag", string.Empty); + string[] replacementValues = replaceElement.GetAttributeStringArray("value", new string[0]); + string replacementValue = string.Empty; + for (int i = 0; i < replacementValues.Length; i++) + { +#if DEBUG + if (!int.TryParse(replacementValues[i], out int _) && !float.TryParse(replacementValues[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out float __) && !ContainsTag(replacementValues[i])) + { + DebugConsole.AddWarning($"Couldn't find the tag \"{replacementValues[i]}\" in text files for description \"{descriptionTag}\". Is the tag correct?"); + } +#endif + replacementValue += Get(replacementValues[i], returnNull: true) ?? replacementValues[i]; + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } + } + if (replaceElement.Attribute("color") != null) + { + string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); + replacementValue = $"‖color:{colorStr}‖{replacementValue}‖color:end‖"; + } + extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + } + if (!string.IsNullOrEmpty(Description)) { Description += "\n"; } + Description += extraDescriptionLine; + } + public static string FormatServerMessage(string textId) { return $"{textId}~"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 1881729df..41039f752 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -147,7 +147,7 @@ namespace Barotrauma /// private static float ApplyPercentage(float value, float amount, int times) { - return times <= 0 ? value : ApplyPercentage(value + (value * amount / 100), amount, --times); + return (1f + (amount / 100f * times)) * value; } public static PropertyReference[] ParseAttributes(IEnumerable attributes, Upgrade upgrade) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 06f4c29fc..2a62d2b4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -68,7 +68,13 @@ namespace Barotrauma Name = element.GetAttributeString("name", string.Empty); IsWallUpgrade = element.GetAttributeBool("wallupgrade", false); - if (string.IsNullOrWhiteSpace(Name)) + string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + if (!string.IsNullOrWhiteSpace(nameIdentifier)) + { + Name = TextManager.Get($"{nameIdentifier}", returnNull: true) ?? string.Empty; + } + else if (string.IsNullOrWhiteSpace(Name)) { Name = TextManager.Get($"UpgradeCategory.{Identifier}", true) ?? string.Empty; } @@ -131,6 +137,8 @@ namespace Barotrauma public string Description { get; } + public float IncreaseOnTooltip { get; } + public string Identifier { get; } public string FilePath { get; } @@ -172,7 +180,13 @@ namespace Barotrauma var targetProperties = new Dictionary(); - if (string.IsNullOrWhiteSpace(Name)) + string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + if (!string.IsNullOrWhiteSpace(nameIdentifier)) + { + Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty; + } + else if (string.IsNullOrWhiteSpace(Name)) { Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty; } @@ -182,6 +196,8 @@ namespace Barotrauma Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty; } + IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f); + DebugConsole.Log(" " + Name); foreach (XElement subElement in element.Elements()) @@ -249,7 +265,7 @@ namespace Barotrauma public bool IsDisallowed(Item item) { - return item.disallowedUpgrades.Contains(Identifier); + return item.disallowedUpgrades.Contains(Identifier) || UpgradeCategories.Any(c => item.disallowedUpgrades.Contains(c.Identifier)); } public static UpgradePrefab? Find(string identifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index af7097579..6e086afb8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -12,7 +12,7 @@ namespace Barotrauma private int maxId; - private readonly List srcRanges; + private readonly List> srcRanges; private readonly int destOffset; public IdRemap(XElement parentElement, int offset) @@ -20,13 +20,13 @@ namespace Barotrauma destOffset = offset; if (parentElement != null && parentElement.HasElements) { - srcRanges = new List(); + srcRanges = new List>(); foreach (XElement subElement in parentElement.Elements()) { int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().Y) + 1; + maxId = GetOffsetId(srcRanges.Last().End) + 1; } else { @@ -44,38 +44,38 @@ namespace Barotrauma { for (int i = 0; i < srcRanges.Count; i++) { - if (srcRanges[i].X > id) + if (srcRanges[i].Start > id) { - if (srcRanges[i].X == (id + 1)) + if (srcRanges[i].Start == (id + 1)) { - srcRanges[i] = new Point(id, srcRanges[i].Y); - if (i > 0 && srcRanges[i].X == srcRanges[i - 1].Y) + srcRanges[i] = new Range(id, srcRanges[i].End); + if (i > 0 && srcRanges[i].Start == srcRanges[i - 1].End) { - srcRanges[i - 1] = new Point(srcRanges[i - 1].X, srcRanges[i].Y); + srcRanges[i - 1] = new Range(srcRanges[i - 1].Start, srcRanges[i].End); srcRanges.RemoveAt(i); } } else { - srcRanges.Insert(i, new Point(id, id)); + srcRanges.Insert(i, new Range(id, id)); } return; } - else if (srcRanges[i].Y < id) + else if (srcRanges[i].End < id) { - if (srcRanges[i].Y == (id - 1)) + if (srcRanges[i].End == (id - 1)) { - srcRanges[i] = new Point(srcRanges[i].X, id); - if (i < (srcRanges.Count - 1) && srcRanges[i].Y == srcRanges[i + 1].X) + srcRanges[i] = new Range(srcRanges[i].Start, id); + if (i < (srcRanges.Count - 1) && srcRanges[i].End == srcRanges[i + 1].Start) { - srcRanges[i] = new Point(srcRanges[i].X, srcRanges[i + 1].Y); + srcRanges[i] = new Range(srcRanges[i].Start, srcRanges[i + 1].End); srcRanges.RemoveAt(i + 1); } return; } } } - srcRanges.Add(new Point(id, id)); + srcRanges.Add(new Range(id, id)); } public ushort GetOffsetId(XElement element) @@ -92,11 +92,11 @@ namespace Barotrauma int currOffset = destOffset; for (int i = 0; i < srcRanges.Count; i++) { - if (id >= srcRanges[i].X && id <= srcRanges[i].Y) + if (id >= srcRanges[i].Start && id <= srcRanges[i].End) { - return (ushort)(id - srcRanges[i].X + 1 + currOffset); + return (ushort)(id - srcRanges[i].Start + 1 + currOffset); } - currOffset += srcRanges[i].Y - srcRanges[i].X + 1; + currOffset += srcRanges[i].End - srcRanges[i].Start + 1; } return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 299e6d6dd..77ddd1d2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -29,6 +29,11 @@ namespace Barotrauma return (i % n + n) % n; } + public static float PositiveModulo(float i, float n) + { + return (i % n + n) % n; + } + public static double Distance(double x1, double y1, double x2, double y2) { double dX = x1 - x2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs new file mode 100644 index 000000000..5d380e221 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Range.cs @@ -0,0 +1,44 @@ +using System; + +namespace Barotrauma +{ + public struct Range where T : IComparable + { + private T start; private T end; + public T Start + { + get { return start; } + set + { + start = value; + VerifyStartLessThanEnd(); + } + } + + public T End + { + get { return end; } + set + { + end = value; + VerifyEndGreaterThanStart(); + } + } + + private void VerifyStartLessThanEnd() + { + if (start.CompareTo(end) > 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.Start set to a value greater than End ({start} > {end})"); } + } + + private void VerifyEndGreaterThanStart() + { + if (end.CompareTo(start) < 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.End set to a value less than Start ({end} < {start})"); } + } + + public Range(T start, T end) + { + this.start = start; this.end = end; + VerifyEndGreaterThanStart(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 563760152..0621dd858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -23,17 +23,19 @@ namespace Barotrauma.IO { path = System.IO.Path.GetFullPath(path).CleanUpPath(); - string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); - if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + if (!isDirectory) { - return false; - } - - if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) - && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) - { - return false; + string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); + if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) + && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) + { + return false; + } } foreach (string unwritableDir in unwritableDirs) @@ -158,6 +160,11 @@ namespace Barotrauma.IO return System.IO.Path.GetPathRoot(path); } + public static string GetRelativePath(string relativeTo, string path) + { + return System.IO.Path.GetRelativePath(relativeTo, path); + } + public static string GetDirectoryName(string path) { return System.IO.Path.GetDirectoryName(path); @@ -246,6 +253,7 @@ namespace Barotrauma.IO if (!Validation.CanWrite(path, true)) { DebugConsole.ThrowError($"Cannot create directory \"{path}\": modifying the contents of this folder/using this extension is not allowed."); + Validation.CanWrite(path, true); return null; } return System.IO.Directory.CreateDirectory(path); @@ -310,7 +318,11 @@ namespace Barotrauma.IO return System.IO.File.GetLastWriteTime(path); } - public static FileStream Open(string path, System.IO.FileMode mode, System.IO.FileAccess access = System.IO.FileAccess.ReadWrite) + public static FileStream Open( + string path, + System.IO.FileMode mode, + System.IO.FileAccess access = System.IO.FileAccess.ReadWrite, + System.IO.FileShare? share = null) { switch (mode) { @@ -326,10 +338,12 @@ namespace Barotrauma.IO } break; } - return new FileStream(path, System.IO.File.Open(path, mode, + access = !Validation.CanWrite(path, false) ? System.IO.FileAccess.Read : - access)); + access; + var shareVal = share ?? (access == System.IO.FileAccess.Read ? System.IO.FileShare.Read : System.IO.FileShare.None); + return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal)); } public static FileStream OpenRead(string path) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 2227bfdf5..a797bde55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -297,20 +297,10 @@ namespace Barotrauma public static void CompressStringToFile(string fileName, string value) { // A. - // Write string to temporary file. - string temp = Path.GetTempFileName(); - File.WriteAllText(temp, value); + // Convert the string to its byte representation. + byte[] b = Encoding.UTF8.GetBytes(value); // B. - // Read file into byte array buffer. - byte[] b; - using (FileStream f = File.Open(temp, System.IO.FileMode.Open)) - { - b = new byte[f.Length]; - f.Read(b, 0, (int)f.Length); - } - - // C. // Use GZipStream to write compressed bytes to target file. using (FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)) using (GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index fe65006e8..de832af9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -12,6 +12,7 @@ using System.Text; namespace Barotrauma { + [Obsolete("Use named tuples instead.")] public class Pair { public T1 First { get; set; } @@ -24,20 +25,6 @@ namespace Barotrauma } } - public class Triplet - { - public T1 First { get; set; } - public T2 Second { get; set; } - public T3 Third { get; set; } - - public Triplet(T1 first, T2 second, T3 third) - { - First = first; - Second = second; - Third = third; - } - } - public static partial class ToolBox { static internal class Epoch @@ -555,15 +542,6 @@ namespace Barotrauma return hex.ToString(); } - public static string ConvertAbsoluteToRelativePath(string path) - { - string[] splitted = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); - string currentFolder = Environment.CurrentDirectory.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Last(); - // Filter out the current folder -> result is "Content/blaahblaah" or "Mods/blaahblaah" etc. - IEnumerable filtered = splitted.SkipWhile(part => part != currentFolder).Skip(1); - return string.Join("/", filtered); - } - public static string EscapeCharacters(string str) { return str.Replace("\\", "\\\\").Replace("\"", "\\\""); @@ -640,11 +618,27 @@ namespace Barotrauma Process.Start(startInfo); } + /// + /// Cleans up a path by replacing backslashes with forward slashes, and + /// optionally corrects the casing of the path. Recommended when serializing + /// paths to a human-readable file to force case correction on all platforms. + /// Also useful when working with paths to files that currently don't exist, + /// i.e. case cannot be corrected. + /// + /// Path to clean up + /// Should the case be corrected to match the filesystem? + /// Directories that the path should be found in, not returned. + /// Path with corrected slashes, and corrected case if requested. public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "") { if (string.IsNullOrEmpty(path)) { return ""; } - path = path.Replace('\\', '/'); + path = path + .Replace('\\', '/'); + if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring("file:".Length); + } while (path.IndexOf("//") >= 0) { path = path.Replace("//", "/"); @@ -659,21 +653,24 @@ namespace Barotrauma return path; } + /// + /// Cleans up a path by replacing backslashes with forward slashes, and + /// corrects the casing of the path on non-Windows platforms. Recommended + /// when loading a path from a file, to make sure that it is found on all + /// platforms when attempting to open it. + /// + /// Path to clean up + /// Path with corrected slashes, and corrected case if required by the platform. public static string CleanUpPath(this string path) { - if (string.IsNullOrEmpty(path)) { return ""; } - - path = path.Replace('\\', '/'); - while (path.IndexOf("//") >= 0) - { - path = path.Replace("//", "/"); - } -#if LINUX || OSX - //required on *nix platforms to load in mods made on Windows - string correctedPath = CorrectFilenameCase(path, out _); - if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; } + return path.CleanUpPathCrossPlatform( + correctFilenameCase: +#if WINDOWS + false +#else + true #endif - return path; + ); } public static float GetEasing(TransitionMode easing, float t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs deleted file mode 100644 index dcd806d14..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Xml.Linq; - -namespace Barotrauma -{ - public static class UpdaterUtil - { - public const string Version = "1.1"; - - public static void SaveFileList(string filePath) - { - XDocument doc = new XDocument(CreateFileList()); - - doc.SaveSafe(filePath); - } - - public static XElement CreateFileList() - { - XElement root = new XElement("filelist"); - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - XElement fileElement = new XElement("file"); - fileElement.Add(new XAttribute("path", GetRelativePath(file, currentDir))); - fileElement.Add(new XAttribute("md5", GetFileMd5Hash(file))); - - root.Add(fileElement); - } - - return root; - } - - public static List GetFileList(XDocument fileListDoc) - { - List fileList = new List(); - - XElement fileListElement = fileListDoc.Root; - - if (fileListElement == null) - { - throw new Exception("Received list of new files was corrupted"); - } - - foreach (XElement file in fileListElement.Elements()) - { - string filePath = file.GetAttributeString("path", ""); - - fileList.Add(filePath); - } - - return fileList; - } - - public static List GetRequiredFiles(XDocument fileListDoc) - { - List requiredFiles = new List(); - - XElement fileList = fileListDoc.Root; - - if (fileList==null) - { - throw new Exception("Received list of new files was corrupted"); - } - - foreach (XElement file in fileList.Elements()) - { - string filePath = file.GetAttributeString("path", ""); - - if (!File.Exists(filePath)) - { - requiredFiles.Add(filePath); - continue; - } - - string md5 = file.GetAttributeString("md5", ""); - - if (GetFileMd5Hash(filePath) != md5) - { - requiredFiles.Add(filePath); - } - } - - return requiredFiles; - } - - private static string GetFileMd5Hash(string filePath) - { - Md5Hash md5Hash = null; - var md5 = MD5.Create(); - using (var stream = File.OpenRead(filePath)) - { - md5Hash = new Md5Hash(md5.ComputeHash(stream)); - } - - return md5Hash.Hash; - } - - public static string GetRelativePath(string filespec, string folder) - { - Uri pathUri = new Uri(filespec); - // Folders must end in a slash - if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - folder += Path.DirectorySeparatorChar; - } - Uri folderUri = new Uri(folder); - return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); - } - - /// - /// moves the files in the updatefolder to the install folder - /// if there's an existing file with the same name in the install folder and it can't be removed, - /// it will be renamed as "OLD_[filename]" - /// - /// - public static void InstallUpdatedFiles(string updateFileFolder) - { - IEnumerable files = Directory.GetFiles(updateFileFolder, "*", System.IO.SearchOption.AllDirectories); - - string currentDir = Directory.GetCurrentDirectory(); - - foreach (string file in files) - { - string fileRelPath = GetRelativePath(file, updateFileFolder); - - if (File.Exists(fileRelPath)) - { - try - { - File.Delete(fileRelPath); - } - - //couldn't delete file, probably because it's already in use - catch - { - string oldFileName = Path.Combine(currentDir, Path.GetDirectoryName(fileRelPath), "OLD_"+Path.GetFileName(fileRelPath)); - - if (File.Exists(oldFileName)) File.Delete(oldFileName); - - File.Move(fileRelPath, oldFileName); - } - } - - string directoryName = Path.GetDirectoryName(fileRelPath); - if (!string.IsNullOrWhiteSpace(directoryName)) - { - Directory.CreateDirectory(directoryName); - } - - - System.Diagnostics.Debug.WriteLine("moving: "+file+" -> "+fileRelPath); - File.Move(file, fileRelPath); - } - - Directory.Delete(updateFileFolder, true); - } - - public static void CleanUnnecessaryFiles(List filesToKeep) - { - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - string relativePath = GetRelativePath(file, currentDir); - - string dirRoot = relativePath.Split(Path.DirectorySeparatorChar).First(); - if (dirRoot != "Content") continue; - - if (filesToKeep.Contains(relativePath)) continue; - - if (Path.GetFileName(file).Split('_').First() == "OLD") continue; - - System.Diagnostics.Debug.WriteLine("deleting file "+file); - - try - { - File.Delete(file); - } - - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine("Could not delete file \"" + file + "\" (" + e.Message + ")"); - continue; - } - } - } - - - public static void CleanOldFiles() - { - string currentDir = Directory.GetCurrentDirectory(); - - IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); - - foreach (string file in files) - { - if (Path.GetFileName(file).Split('_').First() != "OLD") continue; - - System.Diagnostics.Debug.WriteLine("deleting file " + file); - - try - { - File.Delete(file); - } - - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine("Could not delete file \"" + file + "\" (" + e.Message + ")"); - continue; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 73038ebb9..e41b82691 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 879c0d4f2..90a86631d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index ccce067ae..a79c97e86 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 4751d3daf..4d2f9b30f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/TintTest.png b/Barotrauma/BarotraumaShared/TintTest.png new file mode 100644 index 000000000..77afc5ae9 Binary files /dev/null and b/Barotrauma/BarotraumaShared/TintTest.png differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index a3942d1a0..eefe78651 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,218 @@ +--------------------------------------------------------------------------------------------------------- +v0.15.12.0 +--------------------------------------------------------------------------------------------------------- + +- Slightly increased the amount of experience given by missions. +- Fixed crashing when a monster gets huskified. +- Fixed crashing when a client tries to deactivate the husk infection of a character that's been removed. +- Fixed crashing in EnemyAIController.UpdateFollow. +- Fixed console errors when an electrical discharge coil damages a monster that gets instakilled and disappears on death (e.g. swarm feeder). + +--------------------------------------------------------------------------------------------------------- +v0.15.11.0 +--------------------------------------------------------------------------------------------------------- + +- New icons for the new ruin missions. +- Slightly reduced the amount of experience given by missions and increased the amount of experience required to unlock a talent point. +- Made escort missions more common. +- The creature attack keybind is automatically switched from R to the new default keybind F when loading up the new update for the first time. +- Fixes to ruin waypoints. +- Fixes to outdoors pathfinding. +- Fixes to fractal guardians' aiming. + +--------------------------------------------------------------------------------------------------------- +v0.15.10.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed character skills reducing after every round. +- Fixed psychosis artifact doing burn damage when picked up. +- Fixed a bunch of pathfinding issues when bots are trying to navigate out from the ruins and/or return back to the sub. +- Fixed bots not being able to swap oxygen tanks in the ruins. +- Fixed railgun lights having an excessively high power consumption, causing them to immediately drain the supercapacitors. +- Fixed occasional crashes when clearing the item search bar that is already empty in the status monitor. +- Fixed ruin generator sometimes leaving empty space between some of the modules and the hallways connected to them. +- Fixed alien gas vents affecting the monsters inside ruins. +- Fixed background wall not extending all the way to the edge of one of the outpost docking modules. +- Fixed harpoon rope sometimes being drawn when it's already snapped. + +--------------------------------------------------------------------------------------------------------- +v0.15.9.0 +--------------------------------------------------------------------------------------------------------- + +Alien ruin overhaul: +- Overhauled ruins: completely remade sprites, monsters, layouts, items and puzzles. +- New Scan mission: scan an Alien ruin by placing down provided scanners at target locations and take the scanners back to the outpost. +- New Alien Ruin mission: kill the guardians inhabiting the ruin and destroy their pods. +- Added an additional ambience track for the ruins. + +Character overhaul: +- Completely remade character sprites, ragdolls and animations. +- Option to customize the starting crew in the single player campaign. +- More customization options (skin, hair and facial hair colors, more accessories). +- Added a button to randomize character appearance in the character customization menus. + +Health system improvements: +- Streamlined the health interface. +- Allow administering meds by clicking on the "suitable treatments" suggestions in the health interface. +- The health interface displays a prediction of how much a medical item will reduce/increase the afflictions when hovering the cursor over one. +- Certain afflictions can make the characters' face or body change color. +- Physical injuries to the head can cause concussions. +- Improvements to the blood particle effects when a character is bleeding. +- Damage to arms reduces aiming accuracy. +- Crippled legs slow the player down more. + +Talent system: +- The new talent system allows you to unlock things such as special skills, buffs and fabrication recipes in the course of a campaign, with experience points gained from completing missions. +- Three different talent trees for all the character classes. +- Dozens of new items. +- Item quality system: certain talents allow you to fabricate higher-quality versions of items. +- Characters lose some skill points when respawning mid-round. The talent system makes it easier to gain skills and permanent improvements to the character, and this change is intended to balance that out. + +Overhauled status monitor: +- Improved visuals. +- Indicates the locations of the crew's ID cards. +- Indicates the locations of alerts. +- Electrical view, indicating locations and health of junction boxes, reactor and batteries. +- Allows searching for items and indicating the hulls in which they're located. + +Balance changes: +- Reduced loot in wrecks. +- Difficulty affects the amount of loot. +- Reduced the amount of weapons and grenades in wrecks, pirate ships and abandoned outposts. +- Disabled stacking quality-based items (experimental change, feedback is welcome). +- Reduced diving suits damage resistances. +- Buffed vigor and haste. +- Modified characters' base vitalities. +- Adjustments to monster stats. +- Reduced mission experience gains, level difficulty affects mission experience. +- Made welding tools a bit less effective early to compensate for increases to their effectiveness from quality/talents. +- Upgrade system reworked to work better in conjunction with new talents and quality systems. Quality of life upgrades made better or cheaper, hull upgrades are less effective towards the lategame but are better early, reorganized categories. +- Diving suit and human ragdoll damagemodifier changes: the suits now offer less protection, but humans have a bit more natural protection towards physical damage types. +- Adjustments to outpost distribution: natural formations greatly reduced in the 1st zone, cities slightly reduced in the 1st zone, outposts (including specialized ones) increased in the 3rd and 4th zone. +- Made magnesium a little more common in stores and wrecks. + +Additions and changes: +- Gene splicing. You can find alien genetic material inside ruins (and for the time being, wrecks), and use these materials to gain special abilities and buffs. The materials can be processed using a Research Station (which atm can be found in research outposts) and applied on a character using a Gene Splicer. +- Added a new "Return" order for ordering bots to return back to the main submarine. +- Bots can now use level waypoints to help them navigate around when they are outside the submarine. +- Play editor music in the multiplayer lobby. +- Option to specify the amount of items to spawn with the "spawnitem" command. +- Optimized cave vent and ballast flora spore particles. +- Added a 5 second "cooldown" before a junction box broken by overloading can take damage from overloading again. Prevents continuous fires and particles when continuously repairing an overloaded junction box. +- Small monsters don't eat the inventory contents of a character they're eating (the items drop instead). +- Disabled new status monitor features from handheld status monitors. +- Round water and oxygen percentage readings on the status monitor (e.g. 99.999998% shows up as 100% instead of 99%). +- Adjustments to how far creatures can see and hear the submarine and it's devices from. Moving fast now makes more noise, moving slowly less, and the monsters can't see the sub from as far as before. Effectively it should now be more viable tactic to shut the engines down and keep silent. +- Reduce sonar ping's sound range from 10000 to 8000 to make it possible to spot (some) monsters before they target the sub. +- Made a couple of monsters unable to eat characters (hammerheads, terminal cells, leucocytes, molochs, spinelings and watchers). +- Changed default creature attack key to F because R conflicts with the radio keybind. +- Disabled toggling the sonar mode by pressing the Run key. +- Added condition_out pin to various items. +- Bots no longer ignore unconscious targets that regenerate health (i.e. they will finish off downed husks to prevent them from getting back up again). +- Fabricating fuel rods now requires electrical skills instead of mechanical. +- Reactor now requires electrical skills instead of mechanical to repair. +- When the status monitor receives the oxygen/water level for a hull, it registers it on all the linked hulls as well (-> no need to put an oxygen/water detector in all the hulls of a multi-hull room). +- Removed the "burndamage" damage type (not the same as "burn") that was added as a temporary workaround to allow pulse lasers to bypass monster's damage modifiers. +- Changed the look of the skill/xp notifications to accommodate the larger numbers of notifications you can get from talents and skillbooks. +- Added a fabricator and deconstructor to Azimuth and slightly lowered its maximum speed. +- Increased Azimuth's battery out relay max power. +- Temporarily disabled magnesium exploding in water to prevent issues with talents related to it. +- Added "targetlimb" argument to the giveaffliction command (allows applying the affliction to a specific limb). +- Players who wander inside a respawn shuttle don't get automatically killed when the shuttle despawns if they weren't part of the respawning crew. +- Bots no longer ignore severe fires in reactor, engine, or command rooms. The intention for them ignoring the severe fires was to prevent unwanted casualities when the fire can be left untreated and wait for it to fade out when not ordered to extinguish fires. +- Buffs are transferred to AI-controlled husks when a character transforms. +- Projectiles shift to the left in multi-slot loaders when firing. +- Option to make terminals use a monospaced font. +- Player-controlled monsters can now grab and eat bodies. +- Added triangle and sawtooth wave types to oscillator component. +- Added "high_pressure" output to water detector. +- Water detectors round the water percentage output up, so any amount of water will be at least 1%. +- Focus on the password field automatically in the server password prompt and allow submitting it with enter. +- Made pirates a little less accurate when they're operating turrets: they can no longer magically aim exactly at characters inside another sub. +- Biome noise loop volume is tied to sound volume instead of music volume. +- Endworms no longer always bleed to death when their tail is cut. +- Lever state is visualized on its sprite. +- Enabled NVidia Optimus on Windows. + +Fixes: +- Fixed crashing when an attack is applied on a character from a source other than another character, e.g. propeller (unstable only). +- Fixed current_position_y output not working on nav terminals (unstable only). +- Fixed fuel rods having a bullet as a contained indicator (unstable only). +- Removed duplicate welcome messages from humpack's terminal. +- Fixed start and spectate buttons shrinking in the server lobby every time they're hidden and re-enabled. +- Fixed contained items inside contained items not moving when repositioning a container in the sub editor (e.g. when moving a weapon holder that contains a weapon with a magazine). +- Fixed issues with inaccurate tooltips and incorrectly blocked out order nodes in character-specific command interface. +- Fixed contained items' status effects appearing at the top-left corner of the container if the contained items are not visible (e.g. particle-emitting fuel rods would emit the particles from the top-left corner of the reactor instead of the center). +- Fixed hanging wires not getting selected when selecting the items they're connected to. +- Fixed "divide by zero" console error when scaling construction barrier. +- Fixed ability to wire items between two submarines as long as you stay inside the same sub. +- Fixed crew list background blocking mouse input (again). +- Fixed crashing when the majority of the players are controlling characters belonging to a non-player team while the sub is at the end of the level (e.g. if you're alone in the sub and take control of a monster with console commands). +- Fixed cargo missions sometimes only rewarding the players for 1 crate even when transporting more. +- Fixed the "use as treatment" tooltip showing up when trying to drop an item that can't be used as a treatment on the health interface. +- Fixed characters in the transition phase of a husk infection (i.e. after the stinger has appeared) getting stunned at the start of every round. +- Fixed inability to adjust max mission count in a dedicated server. +- Fixed light components staying powered indefinitely when in a container or inventory (didn't seem to be noticeable on any other vanilla items than sonar beacons, which stayed active indefinitely). +- Fixed some outpost events being possible to activate even if the target NPC is dead. +- Fixed ability to swap contained non-interactable items. +- Fixed crash when loading a container that has no containable restrictions and contains items (e.g. if you put items in a deconstructor and start a new round). +- Fixed bots not swapping oxygen tanks when they are outside and going to a target that is inside. +- Fixed issues with bot combat behavior when outside the submarine. +- Fixed ability to hold 2-handed items with one hand by trying to insert them into an occupied slot in a container that can't hold the item. +- Fixed misaligned nav terminal and status monitor in pirate humpback. +- Fixed inability to install/update mods that have periods in the name. +- Fixed nav terminals "current_position_x" output being in pixels when "current_position_y" is in meters. +- Fixed minerals sometimes spawning in unreachable spots in mining missions (on cells that are next to a cave, but at the wrong side of that cell if there's empty space behind it). +- Fixed items' "allow swapping" property being editable in-game. +- Fixed RegEx components with a non-continuous output always sending a signal out after being loaded. +- Fixed pirate subs sometimes spawning inside floating ice chunks. +- Fixed tracer particles not starting from the position of ranged weapons' barrel. +- Fixed inability to open the pause menu when the cursor is over an inventory slot. +- Fixed handcuffs dropping off from characters' hands when they die or turn into a husk. +- Fixed loadsub command. +- Cap the amount argument of the spawnitem command to 100 to prevent freezing/crashing when trying to spawn a ridiculous amount of the item. +- Fixed "infiltration" event getting stuck on one of the conversation options. +- Fixed signal source being wrong on delayed electrical signals (= signals that were delayed for the next frame after they'd passed through 10 steps). Most noticeably affected status monitors that need to know which oxygen/water detector a signal came from. +- Fixed WifiComponents delaying the signals based on the number of receivers, not how many steps the signal has actually taken, contributing to the previous issue. +- Hopefully fixed an oversight in the sub editor where changing ItemComponent colors with the HSV picker would create an error in the console. +- Fixed paralyzant (and many other meds that don't do direct damage) not triggering guards. +- Fixed sonar monitor's UI being unnecessarily small. +- Fixed contained items inside contained items (e.g. magazines in a rifle on a weapon holder) not rotating in the sub editor. +- The overdosed NPC in the "good samaritan" event can't die until the player has triggered the event (completing the event after the NPC had already died made no sense). +- Fixed console errors when an item a bot has been ordered to target was removed between rounds (e.g. an ignore order targeting a mission item that gets removed at the end of the round). +- Fixes to oxygen generator logic: the generator now periodically recalculates how to distribute the oxygen between the vents, as opposed to doing it once at the start of the round. Just doing it once caused issues if there were e.g. vents or doors that are initially open between the rooms. +- Fixed characters sometimes getting "stuck" when swimming in partially filled multi-hull rooms. Happened because the bottom of the current hull was used as the "floor" if the actual floor was too far below, even if there was another hull below the current one, causing the ragdoll to switch to walking animation and being unable to move because it's not touching the floor (unstable only). +- Fixed outpost events always unlocking the same escort mission. +- The hints about flooded rooms and ballast flora aren't shown in ruins, wrecks or enemy subs. +- Fixed "stowaway" event triggering an event cooldown, preventing monsters from spawning at the beginning of the round. +- Fixed clients (excluding the host) always considering friendly fire to be disabled, leading to minor cosmetic desyncs when a player applies afflictions on another one (i.e. there was a brief delay before the afflictions update client-side). +- Fixed inability to apply buffs on the crew when friendly fire is disabled. +- Fixed ItemContainers only applying the StatusEffects from the first matching Containable, even if there's multiple. Prevented the artifact-specific effects of artifact holder from executing. +- Fixed "giveaffliction" command's limbtype argument not working in multiplayer. +- Fixed "linesperlogfile" server setting doing nothing. +- Fixed discharge coils not working when triggered by via a wired button. +- Fixed hatch waypoint and platforms on Remora Drone. +- Memory usage optimizations. +- Fixed bots shooting enemies even when there's a friendly sub between them and the target. +- Bots take their masks off when if they have successfully equipped a suit. +- Fixed a pathfinding issue in Remora caused by too sparse waypoint distribution. +- Fixed disguises not changing the color of a character's name when hovering the cursor over the character. +- Fixed monsters' attack sounds never playing in multiplayer. + +Modding: +- Implemented an item variant system that works similar to the character variants: you can create new items that inherit the properties of another item and only modify specific aspects of it, reducing the amount of duplicate XML code. See "Depleted Fuel Rod" in engineer_talent_items.xml for an usage example. +- Option to configure minimum damage for OnDamage status effects that require a specific type of affliction (see the "vigor on damage" affliction for an usage example). +- Option to make afflictions draw a full-screen overlay when active. +- Option to make property conditionals target contained items using the attribute targetcontaineditem="true". +- Added support for tileable light textures for Structures by using XML element that has the same syntax as does for Items. +- Added "InPressure" property to characters. +- Fixed hidden items appearing in the job loadout preview if there are other items of the same type that are not hidden (didn't affect any vanilla loadouts). +- Removed error message when trying to transfer items to a husk monster and inventory sizes don't match +- Submarine upgrades can be disallowed by category instead of having to do it separately for each upgrade in the sub editor. +- Fixed a modding related crash when trying to apply a property value of a wrong type using status effects. +- Option to create custom husk infections where player control carries over to the transformed creature. +- Display a console warning when an item's deconstruct output defines an out condition and is also set to copy the condition of the deconstructed item. + --------------------------------------------------------------------------------------------------------- v0.14.9.1 --------------------------------------------------------------------------------------------------------- @@ -75,7 +290,7 @@ Fixes: - Fixed hull properties not carrying over when copying hulls in the sub editor. - Fixed occasional "collection was modified" exception in CargoMission.DetermineCargo. Happened if the client received an updated campaign save while trying to load the sub between rounds. - Fixed a broken waypoint in Berilia's cargo bay. -- Fixed seeds sometimes vanishing when trying to plant them in MP. +- Fixed seeds sometimes vanishing when trying to plant them in MP. - Fixed planter boxes displaying the "uproot" message when empty. - Fixed depth charges going through doors and hatches. - Fixed ability to dock docking hatches to ports and vice versa. @@ -187,7 +402,7 @@ Changes: - Increased the stun of smg rounds from 0.125 to 0.15 to give it a bit more stopping power. - Lowered Husks' health regeneration and bleeding reduction. Crawler Husks now regenerate too. Lowered their health a bit to compensate it. - Hammerhead and Golden Hammerhead: Increased health, added some protection on claws and the tail end. Slightly increase the slow swimming speed and animations. The claws don't break anymore when shot with coilgun. -- Hammerheads and Husks don't avoid gun fire anymore. Pets now avoid gun fire. +- Hammerheads and Husks don't avoid gun fire anymore. Pets now avoid gun fire. - Removed Endworm's weak point in the mouth. - Most outpost events no longer trigger automatically, but require interacting with a specific item/character. - Taking items that contain stolen items counts as stealing, so you can't for example put a toolbox inside an outpost cabinet, load it full of items and then take it. @@ -355,7 +570,7 @@ Campaign changes: - Beacon stations are shown on the campaign map. - Visual changes to the map to make it a bit more intuitive. - When you enter a level with a beacon station, you always get an optional side objective to restore it even if you haven't selected a beacon mission. -- Added radiation on the campaign map: the intensity of the radiation around Jupiter is slowly increasing, which is forcing Europans to delve deeper under the ice. In practice, the radiation gradually destroys the outposts starting from the left side of the map, making it more dangerous and costly to stay in these areas. The intention behind this is to prevent players from farming resources indefinitely in the low-difficulty areas of the game before proceeding further. +- Added radiation on the campaign map: the intensity of the radiation around Jupiter is slowly increasing, which is forcing Europans to delve deeper under the ice. In practice, the radiation gradually destroys the outposts starting from the left side of the map, making it more dangerous and costly to stay in these areas. The intention behind this is to prevent players from farming resources indefinitely in the low-difficulty areas of the game before proceeding further. - Gating progress between biomes: you need a certain amount of money or reputation before you can enter the next biome. - Reworked exit points at uninhabited locations: there's a hole/tunnel above the start/end of the level the sub needs to enter to leave. @@ -612,7 +827,7 @@ v0.12.0.1 --------------------------------------------------------------------------------------------------------- - Adjustments and balancing to monster spawns. -- Modifed meds, buffs and poisons fabrication times. +- Modified meds, buffs and poisons fabrication times. - Potential fix to occasional disconnects with an "index was outside the bounds of the array (ENTITY_POSITION)" error message. Happened when lots of items and characters were being created and removed in rapid succession, for example when using turrets against large numbers of enemies. - Fixed a rare crash caused by an "index out of range" exception in Hull.Update after loading or mirroring certain custom submarines. - Fixed submarine class not affecting the depth at which a submarine starts taking pressure damage. @@ -740,7 +955,7 @@ Bots: - Bots should now properly respect ignored targets also when they are already targeting the item when you tell them to ignore it. - Fixed bots not equipping diving gear when the oxygen level is low and there is no leaks/water in the hull, causing them to suffocate. - Fixed bots "forgetting" autonomous operate (operate reactor or steer) orders if the objectives happen to fail. -- Fixed bots sometimes incorrectly abandoning a movement objective, when the path requires a diving gear. +- Fixed bots sometimes incorrectly abandoning a movement objective, when the path requires a diving gear. - Bots should now know how to get back inside through gaps in the hull. - Fixed NPCs not being able to repower the reactor if the player somehow manages to unpower it. - Bots now run when they are ordered to clean up things. @@ -873,7 +1088,7 @@ v0.11.0.10 - Fixed crashing when entering a new level in the campaign when an inactive pump has been infected with ballast flora. - Fixed ballast flora branches respawning instantly if they're destroyed while they're growing towards a target. - Fixed crashing when attempting to place components outside of the submarine in test mode. -- Fixed inability to rewire beacon stations when rewiring is disabled on the server. +- Fixed inability to rewire beacon stations when rewiring is disabled on the server. - Fixed repair tools that aren't held causing a crash upon use (only affects modded items). - Fixed raycast weapons (revolvers, shotguns, SMGs) sometimes not hitting monsters in specific areas outside the sub. - Fixed submarine's price field being difficult to edit in the sub editor due to the value getting clamped above the minimum price while typing in the box. @@ -1525,7 +1740,7 @@ v0.10.3.0 --------------------------------------------------------------------------------------------------------- - Fixed SalvageMission not spawning the item if the mission has been attempted previously, causing an "attempted to pick up a removed item" error when trying to pick up the artifact/logbook. -- Fixed bots going to operate the reactor/navterminal in the main sub when they are inside the outpost. Now they should only be allowed to do this when ordered to. +- Fixed bots going to operate the reactor/navterminal in the main sub when they are inside the outpost. Now they should only be allowed to do this when ordered to. - Fixed mechanic tutorial getting softlocked if the oxygen tanks are put in the deconstructor without putting them in the player inventory first (e.g. by putting them inside a diving mask and moving them from there to the deconstructor). - Fixed "failed to spawn item, component index out of range" error when an item that originally spawned in a container has been moved inside another container whose ItemContainer component doesn't have the same index as the previous one (e.g. when moving items from cabinets in a wreck into a toolbox). - Fixed dialog prompts in the "clownrelations1" and "engineers_are_special" events being displayed to all players in the server. @@ -1678,7 +1893,7 @@ Additions and changes: - Added 2 new moloch variants: Black Moloch and Moloch Pupa. - Reworked Moloch. - Overhauled level layouts and events (longer and more difficult levels). -- Added two new afflictions: medical items and poisons cause organ damage instead of internal damage and explosions cause deep tissue injuries. Both are functionally identical to internal damage, and treated with the same items. +- Added two new afflictions: medical items and poisons cause organ damage instead of internal damage and explosions cause deep tissue injuries. Both are functionally identical to internal damage, and treated with the same items. - Added DXT5 texture compression to reduce memory consumption. Slightly increases loading times; if you're not short on memory, you may want to disable the compression from the game settings. - Added partial dismemberment for live creatures. Currently enabled only for non-humanoids. (Dismembering dead bodies was already in the game). - Destructible shells/armor -> Moloch's shell can now be destroyed. @@ -1713,7 +1928,7 @@ Additions and changes: - Hulls can be multiedited in the sub editor. - Placed down wires can now be re-equipped in the sub editor by double clicking a loose end. - Added charging docks to Remora. -- Adjusted how pixel sizes are converted to meters (which are used to display the submarine's dimensions and distances on the navigation terminal). Previously 100 pixels corresponded to 1 meter, now it's 80px -> 1m, making the human characters about 1.75m tall. +- Adjusted how pixel sizes are converted to meters (which are used to display the submarine's dimensions and distances on the navigation terminal). Previously 100 pixels corresponded to 1 meter, now it's 80px -> 1m, making the human characters about 1.75m tall. - Distance calculations on the navigation terminal take the shape of the path into account instead of just using the direct distance to the target. - Made improvements to the manual order assignment by adding always visible name labels, displaying indicators for characters' current orders, and repositioning the nodes. - Reduced the damage range of fires, characters don't take damage from fires if there's a closed door or a wall in between. @@ -1929,7 +2144,7 @@ Bugfixes: - Fixed enablecheats command not being relayed to server. - Fixed light component and alarm siren/buzzer states occasionally getting desynced. - Fixed inability to enter the sub through very small hulls. -- Fixed antibiotics not giving husk infection resistance when shot from a syringe gun. +- Fixed antibiotics not giving husk infection resistance when shot from a syringe gun. - Fixed text overflows in the player management panel in the server lobby in languages other than English. - Fixed searchlight toggle doing nothing. - Fixed hulls that have minuscule amounts of water in them (too small to be even rendered) being able to trigger InWater effects and water footstep sounds. @@ -2089,7 +2304,7 @@ UI/UX improvements: - Periscopes can be deselected by pressing esc. - Fabricators can pull ingredients directly from the user's inventory without having to place them in the fabricator's input slots. - Lock the on/off switch in the pump interface when the state is controlled by signals, same with the engine slider. -- 1 second cooldown before doors can be opened/closed after someone else has opened/closed them. Makes it less likely for doors to be opened/closed accidentally when multiple people are trying to use them at the same time. +- 1 second cooldown before doors can be opened/closed after someone else has opened/closed them. Makes it less likely for doors to be opened/closed accidentally when multiple people are trying to use them at the same time. - Show a warning if trying to start a campaign for the first time without playing the tutorials. - Diving suits and fire extinguishers are not automatically picked up from the lockers/brackets when clicking on them to make it less likely to accidentally pick them up. Instead, clicking on them opens the inventory of the container, the same way when interacting with e.g. a steel cabinet. - Subinventories (= inventories inside items, toolboxes for example) open/close faster and cannot be interacted with until fully open. @@ -2195,7 +2410,7 @@ AI: - Replaced the generic "cannot reach target" messages with context-specific and more descriptive messages. - Bots now take the other bots into account when they evaluate the importance of the tasks. Fixes multiple bots going to fix the same leaks or repair the same items. - Bots should now abandon the combat objective only when not fleeing from an enemy. If they fail to flee from an enemy, they will fight (or avoid) instead. -- Fixed bots loading the turrets only with the default ammunition. +- Fixed bots loading the turrets only with the default ammunition. - Fixed multiple bots trying to navigate the submarine at the same time. - Fixed pathfinding applying 10x more penalty on vertical distance when the host is outside (should only apply inside). - Fixed bots starting the path from obstructed waypoints or waypoints that are inside when they are outside or vice versa. @@ -2235,7 +2450,7 @@ Monsters: - Fixed Mudraptors sometimes squeezing themselves towards doors without being able to attack them. - Fixed monsters not reacting to being fired with turrets unless they can target the attacker. - Fixed minor slipping in Mudraptor's walking animation. -- Weapons and tools now have ai targets that are only activated when the items are used -> shooting monsters should make you much more attractive target than just swimming peacefully around. +- Weapons and tools now have ai targets that are only activated when the items are used -> shooting monsters should make you much more attractive target than just swimming peacefully around. Multiplayer: - Fixed a bunch of bugs that caused "missing entity" errors. However, there are many different reasons the error can occur, so even though we have not run into the issue anymore during out testing rounds, there is still a chance it may occur in some situations. @@ -2351,7 +2566,7 @@ Electricity fixes: - Fixed LightComponents being toggled twice when they receive a signal to the toggle connection. Misc fixes: -- Fixed "last used" listbox overlapping with the entity visibility tickboxes in the submarine editor on low resolutions. +- Fixed "last used" listbox overlapping with the entity visibility tickboxes in the submarine editor on low resolutions. - Fixed misaligned colliders on the "Shell A Cap 0 deg A/B" wall pieces. - Fixed links disappearing between linked subs and docking ports when loading in the sub editor. - Fixed loading freezing for up to 10 seconds if the game cannot fetch the remote content for the main menu (update notifications, changelogs, etc). @@ -2403,9 +2618,9 @@ Misc additions and changes: - A new submarine, Kastrull. - 6 new traitor missions with much more varied objectives. - More variants of all job outfits. -- 30 new character face sprites. +- 30 new character face sprites. - Overhauled job assignment logic to make the job distribution a little more balanced. Now each client gets assigned one of the spawnpoints (and its associated job) according to their job preference, which means that if the sub for example has 2x as many engineer spawnpoints than medic spawnpoints, there tend to be 2x more engineers. When playing with a mod that adds new jobs to the game, spawnpoints that have no job associated with them are considered spawnpoints for the non-vanilla jobs. - - We would like to get feedback from players about this: does the job assignment seem fair, are people generally getting the jobs they want? + - We would like to get feedback from players about this: does the job assignment seem fair, are people generally getting the jobs they want? - New logic components: sin, cos, tan, asin, acos, atan, modulo, round, ceil, floor and factorial. - Improved autopilot: much faster and less likely to get stuck. - We would like to get feedback from players about this: Is there still incentive to steer manually? @@ -2617,8 +2832,8 @@ tags, it will replace the vanilla wrench. - The husk affliction can now be modified and applied on any character. - Support for multiple simultaneous husk afflictions based on the same system. - Support for multiple husk appendages. Made the attachment limb configurable in the affliction definition. -- Additional content package validity checks during startup: make sure all XML files in the package can -be loaded, and disable the package if they can't. Fixes tons of console errors on startup when a mod with +- Additional content package validity checks during startup: make sure all XML files in the package can +be loaded, and disable the package if they can't. Fixes tons of console errors on startup when a mod with invalid XML files is enabled. - Don't allow publishing workshop items that contain invalid XML files. - Don't allow selecting invalid content packages in the settings menu. @@ -2643,7 +2858,7 @@ drawn on the character. - Exposed structure sound types. - Exposed the mouth position. - Exposed the angular damping and density for limbs (removed mass, which was not used). -- Added a default texture path for ragdolls so that the texture can only be defined once. Limb specific +- Added a default texture path for ragdolls so that the texture can only be defined once. Limb specific texture definitions override this. - Allow to define a group for different species. The characters in the same group are friendly to each other. - DecorativeSprites can now be used on character limbs (like on items). @@ -2665,7 +2880,7 @@ by holding shift. - Added "pause" console command (only usable in single player). Bugfixes: -- Fixed crashing when the recharge speed of a PowerContainer with no interface (e.g. Alien Generator) is +- Fixed crashing when the recharge speed of a PowerContainer with no interface (e.g. Alien Generator) is adjusted by a signal or a bot. - Fixed docking interface becoming active on navigation terminals when any of the submarine's docking ports are close to another docking port, even if the terminal in question is not wired to that port. @@ -2673,18 +2888,18 @@ ports are close to another docking port, even if the terminal in question is not - Fixed clients not creating a download prompt when a sub they don't have is selected by vote. - Fixed traitor missions almost always placing the mission-related items inside the same containers. - Fixed traitor goal durations being displayed as "duration(xx) seconds" instead of "xx seconds". -- Fixed Traitor's "find an item" objectives not being considered complete if the target item is inside +- Fixed Traitor's "find an item" objectives not being considered complete if the target item is inside another item within the traitor's inventory. -- Fixed server using the provided campaign savefile name as-is (without the required .save file extension) -when starting a new campaign through the console. Caused clients to throw "File transfer failed (wrong +- Fixed server using the provided campaign savefile name as-is (without the required .save file extension) +when starting a new campaign through the console. Caused clients to throw "File transfer failed (wrong file extension """!)" errors and prevented them from receiving the save files. -- Fixed servers being able to start the round multiple times by spamming the "start" console command +- Fixed servers being able to start the round multiple times by spamming the "start" console command before loading the round finishes. - Fixed rewiring sound playing whenever a remote player is using a rewireable device. - Fixed subinventories not opening when grabbing another character with no items in the corresponding slot. -- Fixed draggable inventories getting stuck to a half-open state if the item is equipped when +- Fixed draggable inventories getting stuck to a half-open state if the item is equipped when the inventory is opening/closing. -- Fixed light sprites being mirrored when the item is mirrored, even if the mirroring the item's sprite +- Fixed light sprites being mirrored when the item is mirrored, even if the mirroring the item's sprite had been disabled (e.g. junction boxes). - Fixed motion sensor detection area not being flipped when the item is mirrored. - Fixed status monitor not mirroring rooms on the display in mirrored subs. @@ -2695,12 +2910,12 @@ had been disabled (e.g. junction boxes). - Fixed damaged item sprites that are set to fade in according to the damage always being drawn at full opacity. - Fixed spectators being distributed into teams in combat missions, potentially leading to imbalanced crew sizes. - Notify the client using the "togglekarmatestmode" command about the test mode being enabled/disabled. -- Send karma change notifications when karma has changed by 1 unit or more when test mode is enabled, not +- Send karma change notifications when karma has changed by 1 unit or more when test mode is enabled, not just when an action causes an immediate change of 1 unit or more. - Fixed adjacent sprites bleeding into the platform and topwindow sprites. - Fixed autocompleting submarine/shuttle names when using the submarine/shuttle console commands. - Fixed some items (like sonar beacon) attracting monsters even when they're powered off. -- Fixed inability to open the pause menu if an inventory slot had been highlighted when exiting +- Fixed inability to open the pause menu if an inventory slot had been highlighted when exiting the game screen. - Fixed welded door sprites "twitching" when the submarine moves. - Fixed crashing when a character with no hands or arms drops a holdable item. @@ -2708,12 +2923,12 @@ the game screen. - Traitor missions are considered unsuccessful if the objectives cannot be completed (for example if the submarine doesn't have suitable containers to place the traitor items inside). - Fixed the "Barotrauma" title text staying invisible in the main menu when coming back from the credits. -- Fixed clients not refreshing an item's editing hud when a remote player adjusts the values (i.e. if two -players had selected the same lamp and one changed the color value, the other client wouldn't see +- Fixed clients not refreshing an item's editing hud when a remote player adjusts the values (i.e. if two +players had selected the same lamp and one changed the color value, the other client wouldn't see the value change in the editing hud). - Fixed a couple of the additive light sprites being slightly offset from the lamp. -- Clients don't display client-side vitality changes in healthbars until the actual vitality is received -from the server. Fixes health occasionally dropping and then jumping back up if the client predicts damage +- Clients don't display client-side vitality changes in healthbars until the actual vitality is received +from the server. Fixes health occasionally dropping and then jumping back up if the client predicts damage incorrectly (e.g. if a melee attack hits client-side but doesn't server-side). - Fixed character syncing being very inaccurate when switching to freecam and spectating a character far away from other players. @@ -2767,10 +2982,10 @@ Caused potassium and magnesium to explode continuously client-side when immersed - Fixed occasional "error while reading a message from the server" console errors when joining a server. - Fixed clients occasionally timing out and disconnecting during the loading screens. - Fixed character name box resetting in the server lobby when another client joins or disconnects. -- Fixed "collection was modified" error when a client who's been given control of a bot tries to use +- Fixed "collection was modified" error when a client who's been given control of a bot tries to use the report buttons. - Fixed server hanging when a client joins if the server has a very large number of submarines installed. -- Fixed a bunch of client-side console commands crashing the game if the client disconnects while a question +- Fixed a bunch of client-side console commands crashing the game if the client disconnects while a question prompt is active and then enters something in the console. - Fixed full SteamP2P servers not showing up on the server list regardless of the filters. - Fixed "remove ban" and "range ban" buttons in the server settings menu doing nothing. @@ -2782,17 +2997,17 @@ prompt is active and then enters something in the console. - Fixed scroll values resetting in the multiplayer campaign store menu when purchasing/selling items. Miscellaneous improvements: -- Attempting to drop an item to an empty inventory slot that's not of the right type (e.g. trying to put +- Attempting to drop an item to an empty inventory slot that's not of the right type (e.g. trying to put an extinguisher in the normal inventory slots), automatically moves the item to the correct slot if it's free. - Made irrelevant items (light components, lamps, small automated pumps, etc) non-interactable in the tutorials. -- The first patient cannot be healed in the doctor tutorial until the "take the patient to the medbay" +- The first patient cannot be healed in the doctor tutorial until the "take the patient to the medbay" objective has been completed. - The docking interface does not become active in the captain tutorial when leaving the first outpost. - Added sounds for rewiring and repairing. Miscellaneous fixes: - Fixed skills not having any effect on repair durations. -- Fixed inability to enable content packages created for a version prior to 0.9.2.0 (ones that have the +- Fixed inability to enable content packages created for a version prior to 0.9.2.0 (ones that have the content package in the Data/ContentPackages folder instead of in the mod folder). - Use a welding tool icon to indicate damaged walls in the mechanic tutorial, because the generic repair icon caused some players to think the wall needs to be repaired with a wrench. @@ -2800,13 +3015,13 @@ icon caused some players to think the wall needs to be repaired with a wrench. - Reduced repair durations in the engineer/mechanic tutorials. - Fixed draggable subinventories staying visible when stunned. - Fixed light sprites not being scaled in the sub editor when resizing lamps. -- Fixed contained items not changing their position when flipping the container (e.g. oxygen generator +- Fixed contained items not changing their position when flipping the container (e.g. oxygen generator with some tanks inside). - Fixed projectiles emitting sparks when they hit a character (not just when they hit a structure). - Fixed sonar's zoom slider not moving when autodocking zooms in. - Fixed small creatures being unable to reach waypoints. - Fixed bots getting stuck while trying to repair items (in practice hatches) while holding ladders. -- Hid SMG rounds and coilgun bolts from the sub editor because they're not usable by themselves, but are +- Hid SMG rounds and coilgun bolts from the sub editor because they're not usable by themselves, but are spawned automatically when the SMG/coilgun is fired. - Hid the junction box tutorial variant from the sub editor. - Stun guns and darts can be fabricated and purchased. @@ -2816,21 +3031,21 @@ difficult to hit the crawler after it falls to the ground). - Added confirmation popups when exiting the sub editor or character editor to prevent losing unsaved work. - Fixed reactor warning lights being clickable and becoming invisible when pressed. - Fixed CPR button becoming invisible when pressed. -- Fixed water occasionally flowing only through one gap in a hull, for example water only draining -through a hole at a one side of a room even if there is another hole at the other side of the room. +- Fixed water occasionally flowing only through one gap in a hull, for example water only draining +through a hole at a one side of a room even if there is another hole at the other side of the room. - Fixed scrollbars not resizing when filtering lists (e.g. the submarine list in the "new game" menu). - Fixed being able to fire flamers (and other repair tools) through walls and doors. - Fixed flamers working underwater when the water is shallow enough for the character to stand. -- Fixed battery recharge rate slider not moving when the recharge rate is set by an incoming signal +- Fixed battery recharge rate slider not moving when the recharge rate is set by an incoming signal or a remote player. - Fixed some connection panels being rewireable in vanilla subs when a wire is equipped. - Fixed some pre-placed body armors and grenades in vanilla subs having an incorrect scale. -- Fixed crashing when a character gains a skill level on a skill that's not initially configured +- Fixed crashing when a character gains a skill level on a skill that's not initially configured for the character class. -- Fixed "Add File" dialog of the Workshop publish screen always using the mod's root folder as the file +- Fixed "Add File" dialog of the Workshop publish screen always using the mod's root folder as the file path even if the file is in a subfolder. - Fixed contentpackage version number not being updated when updating an older workshop item. -- Fixed subinventories closing when items are dropped into or removed from them, which made reloading +- Fixed subinventories closing when items are dropped into or removed from them, which made reloading weapons or putting several items into a container cumbersome. --------------------------------------------------------------------------------------------------------- @@ -2839,7 +3054,7 @@ v0.9.2.2 Karma improvements: - Attacking someone who has just recently attacked you doesn't reduce karma. -- The karma penalty from attacking someone scales according to their karma (i.e. smaller penalty for +- The karma penalty from attacking someone scales according to their karma (i.e. smaller penalty for attacking a griefer whose karma is low). - Damaging characters by performing CPR doesn't decrease karma. - Fixed karma decreasing when moving a wire from a connection to another. @@ -2851,9 +3066,9 @@ value of 50. Bugfixes: - Fixed mechanic tutorial crashing after the welding task when playing in Russian. -- Less restrictive client name symbol constraints. Fixes clients failing to join servers if their name +- Less restrictive client name symbol constraints. Fixes clients failing to join servers if their name contains Chinese symbols for example. -- Fixed inability to enable/update content packages created for a version prior to 0.9.2.0 (ones that have +- Fixed inability to enable/update content packages created for a version prior to 0.9.2.0 (ones that have the content package in the Data/ContentPackages folder instead of in the mod folder). --------------------------------------------------------------------------------------------------------- @@ -2869,20 +3084,20 @@ v0.9.2.1 - Fixed server crashing when attempting to greet a traitor with no objective. - Fixed dead characters being occasionally selected as traitors. - Fixed info menu still displaying the old traitor objective after a traitor dies and respawns. -- Allow traitors to re-use the sabotage button when someone starts to repair the device without having +- Allow traitors to re-use the sabotage button when someone starts to repair the device without having to re-open the interface. - Fixed grenades exploding multiple times when triggered using a detonator. - Scaled down alien materials (oxygenite, sulphurite, fulgurium, etc) and fixed their collider sizes. - Karma doesn't spontaneously decay/increase when dead. - Increased karma penalties for poisoning someone. - Option to kick clients with low karma from the server and ban them if they get kicked more than x times. -- Space herpes randomly toggles inverted controls on/off to make it more difficult to bypass the effect +- Space herpes randomly toggles inverted controls on/off to make it more difficult to bypass the effect by modifying keybinds. -- Fixed workshop item preview image not being refreshed in the publish tab when adding a preview image +- Fixed workshop item preview image not being refreshed in the publish tab when adding a preview image to an item that doesn't have one. -- Fixed "error loading submarine" console error when creating a copy of a submarine, deleting the original +- Fixed "error loading submarine" console error when creating a copy of a submarine, deleting the original sub and then saving the copy. -- Fixed server logging an "attempted to send a chat message to a null client" error when a client tries +- Fixed server logging an "attempted to send a chat message to a null client" error when a client tries to send a private message to a non-existent client. - Enable dynamic range compression and VOIP attenuation by default. - Fixed inability to extinguish fires through holes in walls. @@ -2902,15 +3117,15 @@ Easier server hosting: host to forward their ports. Expanded traitor feature: -- Multi-step traitor missions that not only make things more interesting to the traitors themselves but +- Multi-step traitor missions that not only make things more interesting to the traitors themselves but also to give the rest of the crew a fighting chance of detecting the traitor. Anti-griefing: -- Karma: a system that detects malicious actions and automatically creates a more challenging experience -for griefers, or potentially triggers a kickban. The feature is completely optional and the server hosts -can decide how aggressively it will react to malicious actions. In a nutshell, you can lose karma for doing -dumb things, karma is regained gradually over time and may be increased more rapidly by doing good things, -thus negating false positives and also giving you the chance to redeem yourself. Lose too much karma and +- Karma: a system that detects malicious actions and automatically creates a more challenging experience +for griefers, or potentially triggers a kickban. The feature is completely optional and the server hosts +can decide how aggressively it will react to malicious actions. In a nutshell, you can lose karma for doing +dumb things, karma is regained gradually over time and may be increased more rapidly by doing good things, +thus negating false positives and also giving you the chance to redeem yourself. Lose too much karma and you'll start to experience increasing levels of inconvenience. - Wire griefing is more difficult: wires have to be disconnected from the connection panels at both ends before they can be removed. Disconnected wires can be seen visibly hanging from the device, with noticeable @@ -2931,10 +3146,10 @@ New submarine: Bugfixes: - Fixed crashing when selecting doors or gaps in the sub editor. - Fixed crashing when combining items inside an ItemContainer (e.g. cabinet, deconstructor). -- Fixed shuttles not being able to redock into some submarines with unconventionally positioned docking +- Fixed shuttles not being able to redock into some submarines with unconventionally positioned docking ports. Specifically, if a port needed to be docked to from below but was positioned above the center of the submarine (or vice versa), the docking interface would not activate on the navigation terminal. -- Fixed docking port using the wrong submarine's position for joint adjustment, causing errors when +- Fixed docking port using the wrong submarine's position for joint adjustment, causing errors when docking a submarine with greater mass to one with a smaller mass. - Fixed an issue that caused occasional ID mismatch errors if a client died during a multiplayer campaign round and disconnected before the round ended. @@ -2946,49 +3161,49 @@ campaign round and disconnected before the round ended. - Fixed inability to combine items in multiplayer. - Fixed occasional "index out of range" errors when loading walls that have been set to a non-default scale in the submarine editor. -- Fixed inability to scroll the crew list with the mouse wheel when the cursor is over certain parts +- Fixed inability to scroll the crew list with the mouse wheel when the cursor is over certain parts of the list. - Fixed sonar pings stopping mid-way when active sonar is turned off, which could be exploited to stop the pings before they reach a monster further away from the sub. - Fixed clients not seeing other characters when entering spectator mode after their character has been eaten by a creature. -- Fixed clients not seeing other characters in spectator mode after the distance between the submarine +- Fixed clients not seeing other characters in spectator mode after the distance between the submarine and the client's corpse gets great enough. -- Fixed clients getting stuck to a non-functional game screen if they start a new round before the ending +- Fixed clients getting stuck to a non-functional game screen if they start a new round before the ending cinematic has finished server-side. - Fixed projectiles hitting a character you're standing next to when firing. - Fixed characters automatically equipping handcuffs (i.e. handcuffing themselves) if they were picked up when the only free inventory space was the hand slots. -- Fixed a rare race condition that occasionally caused the game to crash during the loading screen with +- Fixed a rare race condition that occasionally caused the game to crash during the loading screen with a "no text packs available in English!" error message. -- Fixed husk infection being healable with broad-spectrum antibiotics even when the infection has reached +- Fixed husk infection being healable with broad-spectrum antibiotics even when the infection has reached the final stage. -- Fixed attachable items (detonators, electrical components) becoming deattachable without any tools +- Fixed attachable items (detonators, electrical components) becoming deattachable without any tools if the sub is saved after deattaching them. -- Fixed projectile raycasts not taking wall rotation into account, causing the projectiles to hit sloped +- Fixed projectile raycasts not taking wall rotation into account, causing the projectiles to hit sloped walls when standing next to them. -- Fixed PowerTransfer components checking overloads based on the item the recursive power check starts from, +- Fixed PowerTransfer components checking overloads based on the item the recursive power check starts from, not the item that's currently being checked. Caused junction boxes to never break or catch fire in some subs - Fixed crashing in the character editor when no textures were found for the selected character. - Fixed crashing if a humanoid character has no knees or ankles. - Fixed EventManager calculating flooding amount incorrectly, causing floods to only have a very small effect on the intensity value. -- Fixed batteries and supercapacitors being able to provide power through their signal connections +- Fixed batteries and supercapacitors being able to provide power through their signal connections (e.g. "set_charge_rate", "charge_rate_out"). -- Lighting fix: obstruct background lights behind hulls. Previously the background lights would only get -obstructed by background structures, causing light to "bleed through" parts with no structures (e.g. +- Lighting fix: obstruct background lights behind hulls. Previously the background lights would only get +obstructed by background structures, causing light to "bleed through" parts with no structures (e.g. humpback's docking port) and windows in the background walls. - Fixed item loading being interrupted if any item XML cannot be loaded, causing some items not to be loaded if any of the selected content packages are missing files or contain corrupted item XMLs. - Fixed fire sounds not playing when standing inside a fire source. -- Fixed client names being converted to lower case when comparing them to the name written by the sender, +- Fixed client names being converted to lower case when comparing them to the name written by the sender, preventing the message from being sent if the name wasn't written in lower case. - Pass private messages to the client hosting the server when the message is targeted to the server. - Private messages appear in the server log. - Fixed plasma cutter and welding tool hit particles being offset from the flame. -- Fixed steel bar and titanium-aluminium alloy deconstructing to 100% condition items even when degraded +- Fixed steel bar and titanium-aluminium alloy deconstructing to 100% condition items even when degraded (which could be used as an exploit to get infinite coilgun rounds). -- Fixed holdable items staying in the characters hand(s) when swapping them from a hand slot to another +- Fixed holdable items staying in the characters hand(s) when swapping them from a hand slot to another limb slot (for example, when moving a flashlight from a hand slot to the head slot). - Fixed text wrapping not working properly with words that are longer than a single line. - Fixed deconstructor losing some of the material from deconstructed items if the output container couldn't hold all of the new items. @@ -2996,7 +3211,7 @@ limb slot (for example, when moving a flashlight from a hand slot to the head sl - Fixed a crash when retrieving lobby information with an uninitialized IP. - Fixed max player count in server list & duplicate lobby entries. - Fixed arrow keys not changing caret position when typing. -- Fixed doors looking repaired as soon as the condition goes above 0%, even though the collisions aren't +- Fixed doors looking repaired as soon as the condition goes above 0%, even though the collisions aren't re-enabled until the door gets above 50%. - Fixed water not leaking through broken doors. - Fixed inability to throw items through platforms. @@ -3011,26 +3226,26 @@ from "Content/Characters". Fixes mods not being able to load animation/ragdoll f are not defined explicitly in the character configuration file. - Improved performance by deleting dead monsters that are far away from the sub and by disabling physics on dead bodies when they stay still long enough. -- Added option to use conditionals to activate/deactivate ItemComponents. You can take a look at the +- Added option to use conditionals to activate/deactivate ItemComponents. You can take a look at the reactor and junction box config files to see how it's used. - Added some indicator lights to junction boxes and reactors. - Stun batons don't affect large enemies (molochs, hammerheads, endworms, etc). - In wiring mode, items are selected by pressing E instead of clicking. Selecting items with the left mouse button made it very difficult to manipulate the wires because it was easy to accidentally select -some device instead of a wire node. +some device instead of a wire node. - Crate/toolbox inventories stay open and can be moved across the screen when the item is equipped. - Added limits to submarine name and description length. - Delete incomplete file downloads when disconnecting from a server while a file transfer is active. Prevents console errors about corrupted submarine/save files caused by the partially downloaded files. - Increased Humpback's battery capacity. - Added widgets for manipulating coilgun/railgun rotation limits in the sub editor. -- Added a command for setting the value of a property on all selected entities in the sub editor +- Added a command for setting the value of a property on all selected entities in the sub editor (for example, "setentityproperties scale 2" would set the scale of all selected items/structures to 2). -- Show ballast tanks and airlocks in a different color on the status monitor to make it easier to +- Show ballast tanks and airlocks in a different color on the status monitor to make it easier to distinguish which rooms are actually flooding and which are supposed to have water in them. - Option to define multiple inventory variants for a character (e.g. to add variation to monster loot). - Start playing the main menu music during the loading screen. -- More reliable human walk sounds (played at specific points of the walk cycle instead of relying on +- More reliable human walk sounds (played at specific points of the walk cycle instead of relying on impacts between the feet and the floor). - Option to show a 16x16 grid and snap the cursor to it in the sprite editor. - Reactor can be controlled with movement keys. @@ -3050,7 +3265,7 @@ Additions and changes: - Toolboxes can be fabricated and decontructed. - Nerfed nitroglycerin's structure damage and increased the impact tolerance from 4 to 6. - Made alien pistols a little less underwhelming (sounds, recoil, particle effects). -- Deconstructors drop all items that are inside the deconstructed item. Previously they would just get destroyed, +- Deconstructors drop all items that are inside the deconstructed item. Previously they would just get destroyed, making it a potential exploit to destroy items that should not be possible to deconstruct. - Modified welding fuel tank and oxygen tank sprites a bit to differentiate them from each other a bit more. - Modified plasma cutter and welding tool sprites a bit to differentiate them from each other a bit more. @@ -3058,7 +3273,7 @@ making it a potential exploit to destroy items that should not be possible to de - Server list can be sorted according to ping, name, compatibility or any of the other property in the list. - Docking ports of the enemy submarine are not shown on the sonar during combat missions. - Recharge headset batteries between rounds in single player. -- Disable status monitor displays when out of power. +- Disable status monitor displays when out of power. - Reduce server CPU usage. - Update ladder and gap references if a waypoint is moved around in the editor. - More descriptive error messages when publishing a Workshop item fails. @@ -3078,23 +3293,23 @@ making it a potential exploit to destroy items that should not be possible to de changed by editing a parameter called "allowrewiring" in serversettings.xml. Misc bugfixes: -- Fixed the position of an outpost's docking port not being taken into account when determining how to -place it in the level. Caused large submarines sometimes to collide with walls when docked to outposts +- Fixed the position of an outpost's docking port not being taken into account when determining how to +place it in the level. Caused large submarines sometimes to collide with walls when docked to outposts where the port is offset from the center. - Fixed corrupted sub files causing crashes. -- Fixed item editing HUD not appearing on any other item after one item has been selected in-game +- Fixed item editing HUD not appearing on any other item after one item has been selected in-game (e.g. editing the channel on a wifi component prevented editing other items). -- Fixed ladders and other resizeable items reverting to their original size if they're resized and +- Fixed ladders and other resizeable items reverting to their original size if they're resized and the sub is saved and reloaded. - Fixed inability to resize gaps in the sub editor after they've been placed. -- Reactors take other power sources into account when calculating how much power they need to generate. +- Reactors take other power sources into account when calculating how much power they need to generate. Fixes overloads on Humpback when turning on the backup batteries and operating the reactor normally. - Fixed watchmen not retaliating when a character does very small amounts of damage to them. - Fixed bots being able to take handcuffs off from themselves. - Fixed waypoint and spawnpoints being selectable in the sub editor even if they're hidden. - Fixed deconstructing coilgun ammo boxes giving out more materials than the materials required to fabricate them. - Scrollbars can only be dragged if the mouse button is pressed down while the cursor is on the scrollbar, -not by holding the button and moving it on the scrollbar. Fixes accidentally switching from slider +not by holding the button and moving it on the scrollbar. Fixes accidentally switching from slider to another (often happens in the reactor interface). - Fixed crashing when setting limb or joint scale to 0 using console commands. - Fixed dropdowns in the Workshop item publish menu being draw behind the buttons below them. @@ -3102,7 +3317,7 @@ to another (often happens in the reactor interface). - Ignore keyboard inputs (delete, arrow keys, copy/paste) in the sub editor when a textbox is selected. Prevents accidentally deleting items/structures when attempting to delete text from a textbox. - Toolboxes can't be put inside other toolboxes or in the doctor's clothes. -- Fixed items bought from the store during a singleplayer campaign session being deducted from the credits +- Fixed items bought from the store during a singleplayer campaign session being deducted from the credits when save & quitting from the map. - Flares stop emitting particles when inside an inventory. - Fixed oxygen not flowing through horizontal gaps between subs (e.g. between Remora and the drone). @@ -3113,19 +3328,19 @@ being split into separate buttons when playing the game in Chinese. - Fixed "Text Self_CauseOfDeathDescription.Unknown not found" console error if a character gets killed by being inside a respawn shuttle when it despawns. - Fixed sonar not scaling properly when resolution is changed mid-round. -- Fixed fabricators & deconstructors displaying the "insufficient power" warning when power is low +- Fixed fabricators & deconstructors displaying the "insufficient power" warning when power is low (but still high enough for the devices to run). Networking fixes: -- Fixed another ID mismatch problem that occasionally caused clients to get kicked out in multiplayer, +- Fixed another ID mismatch problem that occasionally caused clients to get kicked out in multiplayer, usually with an error message warning about a missing item. - Fixed players always getting range banned when banned by a client. -- Fixed turrets not being aimed correctly in multiplayer if they're in another sub (e.g. the coilgun in +- Fixed turrets not being aimed correctly in multiplayer if they're in another sub (e.g. the coilgun in Remora's drone). - More reliable fabricator and deconstructor syncing. Should fix items disappearing when multiple players attempt to use the device at the same time and strange timing inconsistencies when deconstructing multiple items in succession. -- Fixed all servers showing "unknown" as the game mode in the server list. +- Fixed all servers showing "unknown" as the game mode in the server list. - Fixed server not allowing forward slashes in host messages. - Fixed repair interface sometimes getting stuck close to 100% in multiplayer. @@ -3135,8 +3350,8 @@ Character editor fixes and improvements: - A dropdown for selecting which content package to add the character to. - Allow creating a new content package during character creation. - Added hotkeys 1, 2, 3 for limb, joint, and animation modes respectively. -- Change the logic of deciding on which parameters are shown and when. In the ragdoll mode you can now -see the ragdoll, but also scale it and see the main parameters. Source rects are now longer shown on the +- Change the logic of deciding on which parameters are shown and when. In the ragdoll mode you can now +see the ragdoll, but also scale it and see the main parameters. Source rects are now longer shown on the sprite sheet if limbs mode is disabled. - Automatically select edit limbs mode when a new character is created. - Option to create multiple limbs in the character creation wizard. @@ -3145,7 +3360,7 @@ sprite sheet if limbs mode is disabled. - The spritesheet is shown by default. - Fixed deleted joints not being saved properly. - Only lock the axis for torso/head position when alt is down. -- Disable test pose if the character is not a humanoid. +- Disable test pose if the character is not a humanoid. - Clamp camera offset so that the character is always at least partially visible. --------------------------------------------------------------------------------------------------------- @@ -3156,26 +3371,26 @@ Changes: - Made husks more common. Multiplayer fixes: -- Fixed an ID mismatch problem that occasionally caused clients to get kicked out in the multiplayer +- Fixed an ID mismatch problem that occasionally caused clients to get kicked out in the multiplayer campaign, usually with an error message warning about a missing item. - Fixed servers using a local campaign save path sent by the client setupping the campaign, preventing the campaign from starting up if the path does not exist at the server's end. -- Fixed radio communication occasionally not working between characters in the multiplayer campaign. Characters -that had made it through at least one round were not able to communicate through radio with characters that +- Fixed radio communication occasionally not working between characters in the multiplayer campaign. Characters +that had made it through at least one round were not able to communicate through radio with characters that have just been spawned for the first time. -- Kick votes persist during a multiplayer session even if the client disconnects (= disconnecting and rejoining +- Kick votes persist during a multiplayer session even if the client disconnects (= disconnecting and rejoining just before you get kicked doesn't work anymore). - The server sends a "this client previously used the name xxx" message when a client rejoins with a different name. -- Fixed GameServer.UnbanPlayer passing the name to BanList in lower case even though BanList was case +- Fixed GameServer.UnbanPlayer passing the name to BanList in lower case even though BanList was case sensitive (preventing unbanning clients with the "unban" command if their name is not in lower case). -- Fixed server not saving the whitelist when it's enabled/disabled, causing the setting to revert when +- Fixed server not saving the whitelist when it's enabled/disabled, causing the setting to revert when relaunching the server. -- Fixed structure texture scale and texture offset now being reverting to default values when the +- Fixed structure texture scale and texture offset now being reverting to default values when the submarine is saved by the server, causing them to reset between campaign rounds. -- Fixed client always launching the default "DedicatedServer.exe" even if using a content package that +- Fixed client always launching the default "DedicatedServer.exe" even if using a content package that should replace the server exe. -- Servers only report content packages with files that cause multiplayer incompatibility to the master server. -Fixes servers showing up as incompatible in the server list if they have custom sub files (or other types +- Servers only report content packages with files that cause multiplayer incompatibility to the master server. +Fixes servers showing up as incompatible in the server list if they have custom sub files (or other types of files that don't cause compatibility issues) installed. - Fixed crashing when clients attempted to kick/ban players through the in-game info menu when a round was running. @@ -3186,11 +3401,11 @@ Misc fixes: - Replaced WinForms with SDL which should resolve most of the fullscreen/resolution issues. It's also now possible to change the resolution when in fullscreen without having to restart the game. - Fixes to the DXGI crashes during startup. -- Fixed Character.GetConfigFile searching for the character file from all available content packages, not -just the ones that are currently selected. Caused modded character files to affect the game even when +- Fixed Character.GetConfigFile searching for the character file from all available content packages, not +just the ones that are currently selected. Caused modded character files to affect the game even when the mod was not enabled, leading to various issues such as characters failing to equip items if the mod changes the number or type of inventory slots a character has. -- Fixed a bug in monster/item spawnpoint logic that occasionally caused perfectly valid spawnpoints to be +- Fixed a bug in monster/item spawnpoint logic that occasionally caused perfectly valid spawnpoints to be discarded when there were floating ice chunks in the level, sometimes causing monsters to spawn very far from the submarine (which often lead to the player not running into them at all). - Fixed bots not being able to retaliate when attacked with a repair tool. @@ -3198,7 +3413,7 @@ far from the submarine (which often lead to the player not running into them at - Fixed items applying their status effects twice if they're put into a container by swapping them with another item. For example, replacing a fuel rod in the reactor by dropping a new fuel rod on it caused the reactor behave as if it had two rods in it. -- Switches send out a continuous 0/1 signal that can be flipped by interacting with the switch (as opposed +- Switches send out a continuous 0/1 signal that can be flipped by interacting with the switch (as opposed to working like buttons which send out a pulse when interacted with). - Fixed an issue that occasionally caused the main menu to look distorted when launching the game on Mac. - Fixed crash after opening the file browser from the Workshop menu more than once on Linux. @@ -3206,20 +3421,20 @@ to working like buttons which send out a pulse when interacted with). - Fixed item scales not being saved when the scale is modified in the sub editor. - Fixed incorrect physicorium shell description. - Fixed server list & workshop menu not resizing properly when changing resolution. -- Fixed player input being used to determine whether a character should be holding on to ladders, causing +- Fixed player input being used to determine whether a character should be holding on to ladders, causing AI characters to let go of ladders when holding RMB. - Fixed explosion damage bypassing armor (both creature shells and wearable items). - Fixed OverrideSaveFolder and OverrideMultiplayerSaveFolder settings not being saved. - Fixed order/rept icons "twitching" when the sub moves. or -- Fixed players being able to repair submerged electrical items indefinitely. -- Fixed a "attempted to access a removed ragdoll" console error when a character wearing the health +- Fixed players being able to repair submerged electrical items indefinitely. +- Fixed a "attempted to access a removed ragdoll" console error when a character wearing the health scanner HUD is removed. - Fixed console errors when a battery's or supercapacitor's capacity is set to 0 and the interface is open. - Fixed ability to drop items into secure lockers (or other containers that require specific items) without having access to it. - Automatically move the gaps linked to doors when the door is moved (and vice versa). - Fixed items disappearing when they're dropped into a full container. -- Fixed crashing when an item that requires aim to use is used by something else than a character +- Fixed crashing when an item that requires aim to use is used by something else than a character (e.g. a status effect). - Fixed having a broken device on a sub in the sub editor and switching to Character Mode causing a crash. - Fixed the bottom of the background ice texture not rendering correctly on high resolutions. @@ -3250,28 +3465,28 @@ other jobs even when there's 3 or less players on the server). - Increased default respawn transport time to 5 minutes, decreased respawn interval to 3 minutes. - Allow muting players mid-round through the info menu. - Allow to edit the door opening/closing speeds and increase the defaults. -- Reduce the follow-up distance and require the bot to be in the same room than the player before stopping, +- Reduce the follow-up distance and require the bot to be in the same room than the player before stopping, so that bots with a follow order don't stay on the doorways when the player is trying to enter the airlocks. - Stop "breathing" deformations when the character is dead. - Disable obstructed paths when the submarine docks to another submarine/outpost. -- The location of save files can be changed in config_player.xml using the attributes "overridesavefolder" +- The location of save files can be changed in config_player.xml using the attributes "overridesavefolder" and "overridemultiplayersavefolder". Bugfixes: - Fixed crashing during startup due to faulty OpenAL installations. - Fixed inability to select the voice capture device if the name of the device contains non-latin symbols (Cyrillic or Chinese characters for example). -- Fixed a networking issue that occasionally caused clients to get kicked with a "disconnected due to +- Fixed a networking issue that occasionally caused clients to get kicked with a "disconnected due to excessive desync" error message. - Fixed fires being very hard to put out completely in multiplayer. - Fixed items not being repaired after purchasing repairs in the campaign. -- Removed outdated Launch_BarotraumaServer script from the Linux version (does not work anymore, the +- Removed outdated Launch_BarotraumaServer script from the Linux version (does not work anymore, the dedicated server should be launched by running the file called "DedicatedServer"). - Fixed server owners not being able to join their own server if whitelist is enabled and the owner is not on the whitelist. - Fixed inability to ban clients by their Steam ID using the console. - Fixed all servers showing up as "Round has not started" in the server list. -- Fixed server ignoring the max players value set in the "host server" menu and using the setting +- Fixed server ignoring the max players value set in the "host server" menu and using the setting configured in "serversettings.xml" instead. - Fixed spectators not hearing the living players' voice chat. - VOIP improvements (less crackling and pops). @@ -3279,24 +3494,24 @@ configured in "serversettings.xml" instead. an oversized crew. - Fixed occasional console errors when ending a round (causing the round end summary not to appear). - Waypoint fixes in vanilla subs. -- Disallow shooting and attacking when the cursor is over a UI element (to prevent, for example, +- Disallow shooting and attacking when the cursor is over a UI element (to prevent, for example, accidentally firing a gun when dismissing a message box). - Fixed "file not found" errors on Linux when attempting to enable a mod that defines file paths using a backslash instead of a slash. - Fixed monsters ignoring decoys. -- Fixed store menu switching back to the equipment category every time something is bought/sold in +- Fixed store menu switching back to the equipment category every time something is bought/sold in the multiplayer campaign. - Fixed "push to talk" field going outside the audio settings menu on some aspect ratios. - Fixed long server names overflowing in the server list menu. - Fixed the character saying "OrderDialogSelf.dismissed" when a player removes an order from themselves. -- Fixed subinventory slots going outside the screen when for example grabbing someone with a toolbox +- Fixed subinventory slots going outside the screen when for example grabbing someone with a toolbox in the leftmost inventory slot. - Fixed monsters sometimes spawning inside the floating ice chunks within the levels. - Fixed physicorium shells not being containable in railgun shell racks. - Fixed physicorium ammo box not being containable in coilgun ammunition shelves. - Fixed inability to spawn items in characters' inventories with the "spawnitem" console command. - Fixed health interface not focusing to the most damaged limb when closing and reopening the interface. -- Prevent welding doors shut during the tutorials (as the player doesn't have access to any tools to +- Prevent welding doors shut during the tutorials (as the player doesn't have access to any tools to reopen the door). - Fixed AI pathfinding when the path is from a submarine to another submarine. - Fixed crashing when AI crew tries to find a path out of the ruins. @@ -3312,21 +3527,21 @@ v0.9.0.4 connection. - Fixed clients occasionally failing to spawn items when playing using a different language than the server, which caused them to get kicked. -- Fixed extra cargo failing to spawn in multiplayer when playing using a different language than the server. -- Fixed legacy items failing to load if a sub is saved with a language other than English and the language +- Fixed extra cargo failing to spawn in multiplayer when playing using a different language than the server. +- Fixed legacy items failing to load if a sub is saved with a language other than English and the language then changed to something else. - Fixed excessively small password input box when connecting to servers. -- Fixed a bug that occasionally caused items to drop from the inventory when moving items between +- Fixed a bug that occasionally caused items to drop from the inventory when moving items between inventory slots in the multiplayer. - Prevent junction boxes from getting damaged due to overvoltage in the engineering tutorial. -- Fixed structures getting scaled incorrectly when cloning a structure with a non-default scale in the +- Fixed structures getting scaled incorrectly when cloning a structure with a non-default scale in the submarine editor. - Fixed occasional crashes when leaving a multiplayer session while the "cinematic" at the end of the round is still playing. - Fixed items not being moved to the humanhusk's inventory when a huskified player dies (= clothes and other gear seemed to magically disappear when the character "resurrected" as an AI husk). - Fixed clients not seeing turrets rotating at their end when another client is operating the turret. -- Fixed hitscan projectiles (revolver rounds) going through walls if the weapon is fired while its +- Fixed hitscan projectiles (revolver rounds) going through walls if the weapon is fired while its barrel is partially inside the wall. - Fixed welding tools being able to weld doors and burn characters through walls. - Fixed bots reporting leaks when there are holes in the interior walls. @@ -3366,7 +3581,7 @@ v0.9.0.2 - Playing the splash screens or tutorial videos doesn't require libvlc and libvlccore to be installed on the user's system in the Linux version anymore. -Bugfixes: +Bugfixes: - Fixed a bug that caused frequent desync kicks when playing a multiplayer monster mission. - Fixed private servers showing up in the server list. - Fixed an index out of range error in DoctorTutorial if proceeding too fast to the submarine. @@ -3401,9 +3616,9 @@ v0.9.0.1 - Fixed docking interface button not working in the multiplayer. - Fixed medical doctor tutorial crashing on languages other than English. - Fixed a bug that caused AI characters to occasionally get stuck next to stairways. -- Fixed animation and ragdoll file paths getting messed up if there are multiple content packages +- Fixed animation and ragdoll file paths getting messed up if there are multiple content packages installed that include monsters with the same name. -- Fixed crashing when a StatusEffect causes an item to be used on a target. +- Fixed crashing when a StatusEffect causes an item to be used on a target. - Fixed a gap between Remora and the drone. - Added more supplies to Remora. - Fixed watchman's dialogue getting muffled. @@ -3437,20 +3652,20 @@ Bugfixes: - Fixes crashing when attempting to use symbols such as <, > or | in the save file name. - UI layout/scale fixes on resolutions larger than 1080p. - Fixed crashing when loading the same sub twice in the sub editor. -- Fixed submarines occasionally being rendered at an incorrect position when viewing them in the sub editor +- Fixed submarines occasionally being rendered at an incorrect position when viewing them in the sub editor after they've been used in-game. - Improved the syncing of ragdolled characters. - Fixed characters not receiving impact damage when ragdolled. - Fixed characters' arms occasionally spinning around when standing still. - Fixed tutorial level generation parameters being used in some normal campaign levels (leading to extremely small levels and submarines overlapping with the outposts). -- Fixed characters running more slowly when their torso is in a different hull than the feet (for example -in Humpback's bilge). +- Fixed characters running more slowly when their torso is in a different hull than the feet (for example +in Humpback's bilge). - Fixed character's feet getting stuck to platforms when climbing ladders while holding A/D. - Fixed monsters being able to drag characters through walls. - Fixed items "vanishing" if they move directly from sub to another without going outside first. - Fixed content package hash calculation failing if the package is not enabled and contains new monster files. -- Fixed inability to enable content packages if some of the files included in the package are already in +- Fixed inability to enable content packages if some of the files included in the package are already in the game folder (which may happen, for example, if enabling a content package fails). - Fixed AllowRagdollButton settings not being synced with clients, leading to strange ragdolling behavior client-side if the server has disabled ragdolling. @@ -3503,7 +3718,7 @@ from getting attacked by a ball of overlapping crawlers. - Fixed huge lag spikes when a character tries to escape from an enemy but can't find a path away from it. - Fixed file transfer progress bars not being visible in the server lobby. - Fixed crashing when attempting to start a mission round with mission type set to None. -- Fixed ElectricalDischarger electricity effect staying visible if the item breaks or the component +- Fixed ElectricalDischarger electricity effect staying visible if the item breaks or the component is deactivated from outside (e.g. via a StatusEffect or the parent component). - Fixed specular maps being rendered on top of characters when outside the sub. - Fixed excessively bright lights around sonar flora and lava vents. @@ -3511,7 +3726,7 @@ is deactivated from outside (e.g. via a StatusEffect or the parent component). - Fixed inability to scroll through long texts in the sub editor's textboxes. - Fixed clients not being able to see other characters in spectator if they've died far away from the sub. - Fixed non-latin characters not being displayed correctly in Workshop item texts. -- Don't prevent selecting items in the sub editor when the cursor is on a wire node, because it makes it +- Don't prevent selecting items in the sub editor when the cursor is on a wire node, because it makes it very difficult (or impossible) to select small items in the wiring mode. - Fixed crashing when attempting to use the "spawnitem" command when a round is not running. @@ -3561,27 +3776,27 @@ v0.8.9.9 Additions and changes: - New control scheme: items are selected by left clicking, deselected with right click or esc, and held -items are used on devices by pressing E (e.g. when rewiring with a screwdriver or repairing something +items are used on devices by pressing E (e.g. when rewiring with a screwdriver or repairing something with a wrench). The new controls are somewhat experimental; the intention is to make them more intuitive to new players. You can still switch back to the legacy control scheme from the game settings. - Set default radio chat keybind to R and creature attack keybind to Mouse3. -- MODDERS, PLEASE NOTE: Moved crafting recipes from the fabricator xml to the xmls of the items. Makes it -possible for modders to add new craftable items without having to modify the fabricators. +- MODDERS, PLEASE NOTE: Moved crafting recipes from the fabricator xml to the xmls of the items. Makes it +possible for modders to add new craftable items without having to modify the fabricators. - Some menu layout improvements. -- Camera movement is disabled completely when an item interface is open (not just when the cursor is on +- Camera movement is disabled completely when an item interface is open (not just when the cursor is on the interface). - Option to disable the camera pan/zoom effects from the game settings. - Option to set a custom preview image for subs. - Allow aiming on ladders when not moving. - Characters play Entrance of the Gladiators on the guitar when wearing a clown mask. -- Display a warning on the status monitor when docked to an outpost ("Docked to X, undock before attempting +- Display a warning on the status monitor when docked to an outpost ("Docked to X, undock before attempting to maneuver the submarine"). - Improvements to the line of sight effect. Prevents ugly-looking artifacts in spots where two wall pieces meet. -- The server gives the "None" permissions to new clients, allowing server hosts to automatically give +- The server gives the "None" permissions to new clients, allowing server hosts to automatically give specific permissions to all clients. - Increased submarine masses to make it less easy for characters to push them around. -- Ping direction is shown on the sonar display when adjusting the direction slider even if directional ping +- Ping direction is shown on the sonar display when adjusting the direction slider even if directional ping is not enabled. - Tweaked charybdis' AI, attacks and animations. - Nuclear explosions cause radiation sickness. @@ -3594,11 +3809,11 @@ is not enabled. - Added blood particle effects when under high pressure. - Some optimization to reduce loading times. - Added a search bar to fabricators. -- Increased the range of docking port sounds and added a subtle camera shake when locking the ports to make +- Increased the range of docking port sounds and added a subtle camera shake when locking the ports to make it more noticeable when a sub docks. - Made all new medical items fabricable. -- Automatically put the currently equipped item in the inventory (no matter if it's one or two handed) when -picking up items that require two hands. +- Automatically put the currently equipped item in the inventory (no matter if it's one or two handed) when +picking up items that require two hands. - Job preferences can be edited mid-round in the info menu. - Slightly reduced the amount of oxygen characters consume from hulls. - Enemies don't attack outposts or targets inside it anymore. @@ -3608,31 +3823,31 @@ Multiplayer fixes: keep welding, honking a bike horn or whatever else they were doing until the server kills the character. - More reliable throw StatusEffect (= grenade explosion) syncing. Fixes clients not seeing explosions at their end. -- More reliable item wall attaching syncing. -- Servers don't attempt to send position updates for items that have no enabled physics body (e.g. attached +- More reliable item wall attaching syncing. +- Servers don't attempt to send position updates for items that have no enabled physics body (e.g. attached items). Fixes "received a position update for an item with no physics body" console errors when attaching items to walls. - Fixed spectate button staying visible when a round ends while a client is in the lobby. - Fixed remote characters sliding slowly to the left client-side when standing in place. -- Fixed server creating "attempted to create a network event for an item that hasn't been fully initialized +- Fixed server creating "attempted to create a network event for an item that hasn't been fully initialized yet" console errors when spawning LightComponents mid-round. - Fixes monsters flipping around way too often client-side (especially when inside the sub). Bugfixes: -- Fixed wire connections that have been done mid-round not working properly. +- Fixed wire connections that have been done mid-round not working properly. - Fixed crashing when attempting to speak as a monster in single player. - Fixed linked subs not getting docked correctly when loading a saved game. - Fixed turrets not working if they're placed inside the submarine. - Fixed calyxanide not being usable in syringe guns. - Explosive harpoons disappear after exploding. -- Emptying the "required items" field of an item in the sub editor now removes the item requirements (instead +- Emptying the "required items" field of an item in the sub editor now removes the item requirements (instead of using the default ones). - Fixed crashing if a fabricator finishes creating an item after the user has been removed (e.g. eaten). - Fixed crashing if none of the selected content packages contain location portraits suitable for the main menu. - Fixed projectiles not applying status effects on impact if they have no attack defined. - Fixed thorium rods not being usable in the reactor. -- Conditionals return a match when checking status tag inequality and the target has no status tags (e.g. -checking if a character doesn't have a StatusEffect with a "poison" tag returns true even if the character +- Conditionals return a match when checking status tag inequality and the target has no status tags (e.g. +checking if a character doesn't have a StatusEffect with a "poison" tag returns true even if the character has no active StatusEffects). - Fixed severed limbs occasionally noclipping into the submarine. - Fixed large engine emitting smoke before it becomes repairable. @@ -3642,12 +3857,12 @@ v0.8.9.8 --------------------------------------------------------------------------------------------------------- Additions and changes: -- Improved tutorial - better videos, instructional texts, objective list that suggest what you should do +- Improved tutorial - better videos, instructional texts, objective list that suggest what you should do next, option to rewatch the videos and re-read the instructions. - Overhauled charybdis (still a work in progress though). -- Automatically grab adjacent ladders when the top/bottom of the current ladder is reached. Makes moving -through docking ports a little less confusing. -- Option to configure when afflictions become visible with the health scanner by adding +- Automatically grab adjacent ladders when the top/bottom of the current ladder is reached. Makes moving +through docking ports a little less confusing. +- Option to configure when afflictions become visible with the health scanner by adding a "ShowInHealthScannerThreshold" attribute to the affliction. - Added labels next to periscopes in Humpback and Dugong. - Modified Humpback's bilge to make it easier for AI characters to fix. @@ -3660,14 +3875,14 @@ a "ShowInHealthScannerThreshold" attribute to the affliction. the wires without having to disconnect them). - Decreased structure damage done by frag grenades and made them disappear after they've exploded. - Batteries output charge values as integers. -- Made damaged junction boxes less sensitive to overvoltage. Nearly broken junction boxes were barely able -to handle any overvoltage, leading to chain reaction where one junction box breaking causes the grid to be +- Made damaged junction boxes less sensitive to overvoltage. Nearly broken junction boxes were barely able +to handle any overvoltage, leading to chain reaction where one junction box breaking causes the grid to be overloaded, and the rest of the boxes start taking damage at an increasing speed. - Reactors don't cool down when underwater anymore. -- Removed minimum conditions from battery deconstruction output (= deconstructing an empty battery still +- Removed minimum conditions from battery deconstruction output (= deconstructing an empty battery still gives the materials used to craft the battery). - Made a bunch of ItemContainer UI panels larger. -- Items can be dragged and dropped directly from the inventory into containers without having to select +- Items can be dragged and dropped directly from the inventory into containers without having to select the container first. - Plants can be picked up from the environment without any tools. - Added more help texts to highlighted items ("[E] Interact", "[E] Climb"...) @@ -3681,24 +3896,24 @@ excessive amounts of network events. - Fixed clients being unable to start a campaign using a submarine that's not in the default Submarine folder at the server's side. - Fixed loading submarine files and campaign saves occasionally failing when running multiple instances -of the game from the same install location (for example, a dedicated server executable and a client +of the game from the same install location (for example, a dedicated server executable and a client executable). -- Don't transfer files through the network when sending them to the owner of the server (i.e. a client +- Don't transfer files through the network when sending them to the owner of the server (i.e. a client hosting directly from the main executable). - Fixed fires and water occasionally getting out of sync between a client using the fire/water console commands and the server. - Fixed clients disconnecting with an "unknown object header" error if they fail to read a network event (when they should instead report the error to the server and wait for a message that contains a more descriptive error). -- Campaign fix: clear missions from locations that change their type, and all adjacent locations. Not -clearing them caused missions to still be available when they logically shouldn't be (e.g. a transport +- Campaign fix: clear missions from locations that change their type, and all adjacent locations. Not +clearing them caused missions to still be available when they logically shouldn't be (e.g. a transport mission from an uninhabited location to another) and syncing issues in multiplayer. - Disable campaign start button if a round is already running when joining. -- Fixed clients being unable to end campaign rounds at all if the sub isn't at the start/end outpost +- Fixed clients being unable to end campaign rounds at all if the sub isn't at the start/end outpost (regardless if they have the permission to end the round or not). -- Fixed campaign characters still being displayed in the server lobby after the game mode has been +- Fixed campaign characters still being displayed in the server lobby after the game mode has been changed to something else. -- Fixed items in the characters inventory always starting at 100% condition client-side even if they had +- Fixed items in the characters inventory always starting at 100% condition client-side even if they had deteriorated during the previous round. - Fixed LevelResource (mineral, plant, etc) deattach timers not being synced with clients. - AI characters can take out excess fuel rods from the reactor when needed. @@ -3707,8 +3922,8 @@ deteriorated during the previous round. Bugfixes: - Fixed almost all items using default repair duration values (10 seconds with high skills, 100 seconds with low skills) instead of the ones configured in the item XMLs. -- Nuclear shells and nuclear depth charges disappear after they've exploded. -- Fixed "trying to add a dead character to crewmanager" errors when attempting to revive a character +- Nuclear shells and nuclear depth charges disappear after they've exploded. +- Fixed "trying to add a dead character to crewmanager" errors when attempting to revive a character killed by some other affliction than internal damage, bleeding or burns. - Take the position of a sub's docking port into account when determining where to place outposts. Previously the outposts were simply placed midway between the adjacent walls, which occasionally caused @@ -3716,7 +3931,7 @@ problems with submarines whose docking port is close to the bow or tail. - Fixed a bug in relay components that caused a bunch of issues in power grids that utilize relays: Relays would receive the full amount of power from the grid regardless of the load of the devices connected to the power_out connection, causing unnecessary overloads and fires. -- Fixed batteries being able to draw power through relay components that are connected directly to +- Fixed batteries being able to draw power through relay components that are connected directly to a power source, even if the relay isn't on. - Don't allow steering the sub with WASD when a textbox is selected. - Use the SpriteColor of the item when drawing the moving parts of turrets and doors. @@ -3727,16 +3942,16 @@ something inside the sub. - Fixed characters always being created in the default folder in the character editor. - Monsters don't target doors/hatches at the exterior of the sub when inside or inner doors when outside. - Don't display disabled limbs on sonar (i.e. severed limbs that have "faded out"). -- Close the save/load dialogs when leaving the sub editor. Otherwise they'll still be visible when +- Close the save/load dialogs when leaving the sub editor. Otherwise they'll still be visible when re-entering the editor, and saving at that point will overwrite the previously loaded sub with an empty one. -- Removing an item after it's been combined doesn't trigger the OnBroken StatusEffects (e.g. combining two +- Removing an item after it's been combined doesn't trigger the OnBroken StatusEffects (e.g. combining two half-full flash powder jars doesn't cause them to explode). - Fixed welding tools and plasma cutters not hitting targets if the barrel is inside the target (e.g. if trying to weld a completely broken wall with the cutter partially inside the wall). - Fixed very small mineral colliders that made them extremely hard to hit with the plasma cutter. - Fixed items with no sprite crashing the game (now they just cause a console error). -- Don't allow autointeracting with contained items (e.g. picking up an ammunition box from a loader) -if another item is currently selected. Makes it less likely for players to accidentally pick up items +- Don't allow autointeracting with contained items (e.g. picking up an ammunition box from a loader) +if another item is currently selected. Makes it less likely for players to accidentally pick up items from containers when they deselect another item. - Fixed characters not letting go of the character they're grabbing when the health interface is closed by clicking outside the window. @@ -3754,7 +3969,7 @@ Additions and changes: - Clients communicate syncing errors to the server, and the server logs a more descriptive error about what went wrong. Should make it easier to diagnose disconnection issues from now on. - Ending a multiplayer campaign round by talking to watchman doesn't require any special permissions. -- Server automatically ends rounds if there have been no players alive in 60 seconds and respawning +- Server automatically ends rounds if there have been no players alive in 60 seconds and respawning is not allowed during the round. - Added a button for resetting an entity's properties to the default values to the sub editor. - Updated handheld sonar UI graphics. @@ -3764,11 +3979,11 @@ Bugfixes: - Fixed a networking bug that caused the server to send item state changes to the clients before sending a message about the item being spawned. For example, spawning any item with a LightComponent would always cause clients to get disconnected. -- Changes to the way the clients are put in sync with the server when joining mid-round. Should make it +- Changes to the way the clients are put in sync with the server when joining mid-round. Should make it less likely for clients to get disconnected immediately after starting a round. -- StatusEffects only apply non-limb-specific afflictions to one limb even if targeting the whole character. -Fixes drugs like fentanyl and morphine being way too harmful due to the oxygen loss affliction being -applied once per every limb. +- StatusEffects only apply non-limb-specific afflictions to one limb even if targeting the whole character. +Fixes drugs like fentanyl and morphine being way too harmful due to the oxygen loss affliction being +applied once per every limb. - Fixed TargetItemComponentName not working in StatusEffect conditionals (making it impossible to create conditionals that target a specific component of an item). - Made all of the new medical items combinable and usable in a syringe gun (assuming the drug is in a syringe). @@ -3780,12 +3995,12 @@ conditionals that target a specific component of an item). - Fixed flares not activating by left clicking. - Fixed affliction icons flickering rapidly in the health interface and above the health bar if their strength is fluctuating around the threshold where the icon becomes visible. -- Fixed dedicated server crashing when typing in more text than can fit on one line. -- Fixed enemies "fleeing" after they have been shot. There was a steering issue when they targeted characters +- Fixed dedicated server crashing when typing in more text than can fit on one line. +- Fixed enemies "fleeing" after they have been shot. There was a steering issue when they targeted characters inside the sub while being outside. - Fixed Hammerhead attack causing warping. - Fixed incorrect submarine and level seed in server logs when playing campaign mode. -- Hide the start button from the campaign UI if the client doesn't have the permission to manage +- Hide the start button from the campaign UI if the client doesn't have the permission to manage the campaign or rounds. --------------------------------------------------------------------------------------------------------- @@ -3805,8 +4020,8 @@ themselves alive and less likely to get stuck. - New signal items (divide, multiply, subtract, memory, equals, greater than, color, xor). - Option to adjust microphone volume in multiplayer. - Added a console commands for changing the gender and race of the character. -- More intuitive BrokenSprite condition logic: a BrokenSprite with a MaxCondition of 50 will start -fading in at 50 (and be fully visible when the condition drops to 0 or down to the MaxCondition of +- More intuitive BrokenSprite condition logic: a BrokenSprite with a MaxCondition of 50 will start +fading in at 50 (and be fully visible when the condition drops to 0 or down to the MaxCondition of the next BrokenSprite). - Added Mirror X/Y buttons to editing HUDs and tooltips that tell about the keyboard shortcuts. @@ -3821,15 +4036,15 @@ knowing that you'd dropped it. - Fixed "play yourself" always toggling to true when a round ends. - Fixed missing item names in the extra cargo menu. - Fixed traitor rounds failing to start if the server is not hosted by a client. -- Fixed console command aliases not being taken into account in GameClient.HasConsoleCommandPermission -(meaning that the client needed a permission for each name variant of a command, making it impossible +- Fixed console command aliases not being taken into account in GameClient.HasConsoleCommandPermission +(meaning that the client needed a permission for each name variant of a command, making it impossible to for example use "fixwalls" instead of "fixhulls"). -- Made the "control" console command usable to clients. -- Show the "ready to start" tickbox in the server lobby even if the client has the permission to start +- Made the "control" console command usable to clients. +- Show the "ready to start" tickbox in the server lobby even if the client has the permission to start the round. - Fixed server lobby screen not showing the names of the submarines the client doesn't have. - Fixed inability to select the respawn shuttle as a client host. -- Fixed VoipCapture creating new "could not start voice capture" popups constantly if there's no +- Fixed VoipCapture creating new "could not start voice capture" popups constantly if there's no suitable capture device. - Fixed crashing when starting a round if a submarine name contains underscores. - Fixed clients console errors when attempting to modify the properties of an ItemComponent in-game @@ -3840,29 +4055,29 @@ suitable capture device. Misc bugfixes: - Audio fixes (less snap, crackle and pop). - Fixed particle "jitter" when the submarine was moving fast. -- Fixed damage modifiers affecting all afflictions if they use affliction types instead of affliction -identifiers. +- Fixed damage modifiers affecting all afflictions if they use affliction types instead of affliction +identifiers. - Fixed end round vote text going outside the screen if there's a 2-digit amount of votes. - Fixed StatusEffects only applying afflictions to one limb even if the target is "Character" instead of "Limb". - Disable audio instead of crashing if no audio device is found. - Fixed item interfaces getting repositioned every frame when the editing HUD is open. -- Fixed held items clipping with the sleeves of the character (e.g. when holding a revolver while an +- Fixed held items clipping with the sleeves of the character (e.g. when holding a revolver while an uniform is equipped). - Fixed being able to levitate by spamming the ragdoll button. - Fixed dead characters draining oxygen tanks inside diving suits/masks. - Fixed reactor gauges getting messed up if the optimal fission rate is more than 100% (which may happen if the power consumption is larger than what the reactor can generate). - Fixed mud raptors not having an inventory (nor lootable items). -- Fixed inability to interact with any items when aim assist is set to 0%. -- Fixed info panel flickering out and Tab getting "inverted" (= info panel shown when tab is not being held) +- Fixed inability to interact with any items when aim assist is set to 0%. +- Fixed info panel flickering out and Tab getting "inverted" (= info panel shown when tab is not being held) when selecting crew members in the panel. - Fixed characters arms occasionally getting stuck above their shoulders. -- Fixed wire nodes occasionally being created at the wrong end of a wire (e.g. when moving a wire between -connections in a connection panel, the wire stretched from the device at the other end of the wire to +- Fixed wire nodes occasionally being created at the wrong end of a wire (e.g. when moving a wire between +connections in a connection panel, the wire stretched from the device at the other end of the wire to the device that's being rewired). Misc: -- Changed the way arguments are given to the "setclientcharacter" command (no semicolon to separate the +- Changed the way arguments are given to the "setclientcharacter" command (no semicolon to separate the names, quotation marks have to be used for multi-word names just like with any other command). - Show the amount of credits in the crew tab of the campaign menu. - Don't spawn new monsters if docked to the start outpost or within 50 meters of the start/end of the level. @@ -3880,7 +4095,7 @@ Bugfixes: used to fetch the texts from the language files instead of the actual texts). - Fixed AI orders that target a specific item (such as the order to power up the reactor) not working in multiplayer. -- Fixed crashes when attempting to use voice capture or change voice capture settings when there are no +- Fixed crashes when attempting to use voice capture or change voice capture settings when there are no suitable capture devices available. - Fixed clients not being notified when an AI character shuts down the reactor. - Fixed deconstructors staying active without power in multiplayer. @@ -3908,13 +4123,13 @@ two separate server applications. - Option to randomize your job preferences in the server lobby. - Fixed a server timing issue that occasionally caused the server to kick clients due to desync when a round starts. - Fixed occasional server-side "maximum packet size exceeded" errors. -- Require the players to either dock with the ending outpost or to get the sub close and enter the outpost before +- Require the players to either dock with the ending outpost or to get the sub close and enter the outpost before automatically ending the round. Bugfixes: - Fixed crashing if the round ends while the health window is open. -- Fixed incorrect item panel positioning in the crew command interface when the sub is docked to something. -- Fixed crashing when an incompatible content package is selected in config.xml or if the content package +- Fixed incorrect item panel positioning in the crew command interface when the sub is docked to something. +- Fixed crashing when an incompatible content package is selected in config.xml or if the content package cannot be found. - Fixed screen distortion effects on Linux. - Fixed non-character key input on Linux (arrow keys, tab, etc). @@ -3928,21 +4143,21 @@ slots in uniforms. - Fixed AI not reloading coilguns if an empty box of ammunition is inserted in the loader. - Fixed incorrect deusizine scale. - Fixed turret light toggle not doing anything. -- Fixed character skills that aren't defined in the job xml never increasing, resulting in all jobs except +- Fixed character skills that aren't defined in the job xml never increasing, resulting in all jobs except the captain always having a helm skill of 0. - Fixed flashlight & scooter light cones being "clipped". - Fixed StatusEffects bypassing limb damage modifiers. - Fixed waypoints not getting connected between docking ports on some subs. - Fixed target identifiers being bypassed when a StatusEffect is set to target nearby items or characters. -- Fixed the "insufficient skills to use the item" text popping up if a character doesn't have sufficient -skills to operate one of the item's components, even if the component was not interacted with (e.g. captains -got a warning about not being able to use the connection panel of a nav terminal, even if they didn't select +- Fixed the "insufficient skills to use the item" text popping up if a character doesn't have sufficient +skills to operate one of the item's components, even if the component was not interacted with (e.g. captains +got a warning about not being able to use the connection panel of a nav terminal, even if they didn't select the connection panel). Steam Workshop: - Update installed workshop items automatically on startup. - Allow adding submarines to workshop items with the "add file" dialog. -- If creating an update for a workshop item that's currently installed, use the installed version instead +- If creating an update for a workshop item that's currently installed, use the installed version instead of the one downloaded from the workshop. Additions: @@ -3954,20 +4169,20 @@ Additions: - Display linked hulls as one room on the status monitor. - Tons of new sound effects. - Display the controlled character in the crew interface. -- Option to "give orders" to the character you're controlling. In single player it can be useful if you want -the controlled character to keep doing something when switching to another one, in the multiplayer it can be +- Option to "give orders" to the character you're controlling. In single player it can be useful if you want +the controlled character to keep doing something when switching to another one, in the multiplayer it can be used to let others know what you're doing. - Added a weak spot to Moloch's bladder. -- Baby Moloch, doo doo doo doo doo doo +- Baby Moloch, doo doo doo doo doo doo - Added damage particles to Mud Raptors and Molochs. -- Added "minimum velocity" property to to motion sensors. Allows making sensors that, for example, keep a door open +- Added "minimum velocity" property to to motion sensors. Allows making sensors that, for example, keep a door open when a character is standing in the doorway. - Option to choose whether to use AND/OR logic in StatusEffects with multiple conditionals. Defaults to AND. -- Added a 1 second "cooldown" to water detector state switches to prevent alarms from toggling on and off constantly +- Added a 1 second "cooldown" to water detector state switches to prevent alarms from toggling on and off constantly when the water level is fluctuating around the position of the detector. - Added scram option (reactor shutdown) to the nav consoles in the vanilla subs. - Support for binding Mouse4, Mouse5 and MouseWheel. -- Made Hammerhead and Mudraptor attracted to light. +- Made Hammerhead and Mudraptor attracted to light. - New husk sprite (still WIP). Misc: @@ -3977,17 +4192,17 @@ repaired. - Miscellaneous optimization. - Removed the info button from the top-left corner - the info menu is now opened with TAB. - Changed default chat/radio keybinds to T and Y. -- Welding tools repair all the walls within the range of the raycast, not just the first wall the raycast hits. +- Welding tools repair all the walls within the range of the raycast, not just the first wall the raycast hits. Makes it easier to repair overlapping and multi-layered walls. - Decreased the range of passive sonar - previously there was often no reason to use the active sonar because the passive mode showed the area around the sub so clearly. -- Health scanner shows all active afflictions (not just those that are visible in the health interface). +- Health scanner shows all active afflictions (not just those that are visible in the health interface). Allows detecting afflictions at an earlier stage, making the item much more useful. - Nerfed the structure damage done by Molochs and Crawlers. - Reduced creature HP across the board. - Increased the amount of minerals in levels. - Increased flare burn time, making them more useful as path markers during exploration of ruins. -- RepairTool damage is configured using StatusEffects and Afflictions instead of the "limbfixamount" attribute +- RepairTool damage is configured using StatusEffects and Afflictions instead of the "limbfixamount" attribute that always does burn damage. - Made headsets craftable. - Battery output doesn't start dropping until the charge is below 10%. @@ -4016,17 +4231,17 @@ mission configuration despite the item being removed. - Made coilgun ammunition boxes craftable and purchaseable, coilgun bolts cannot be purchased anymore. - Fixed AI-controlled husk not spawning when a huskified player dies. - Fixed AI crew occasionally going outside to fix leaks. -- Fixed server failing to sync clients who join the server after a character has been removed during +- Fixed server failing to sync clients who join the server after a character has been removed during the round (e.g. eaten, turned into a husk). - Fixed server-side console errors when clients attempt to use a fabricator. - Display Steam authentication errors in the server logs. - Fixed status effects with a ReduceAffliction value of 0 freezing the game. - Fixed sliders not moving in the battery/supercapacitor interface when an AI character is operating it. -- Fixed chatbox being deselected in the net lobby when receiving a lobby update from the server (i.e. +- Fixed chatbox being deselected in the net lobby when receiving a lobby update from the server (i.e. whenever the server host changes any setting). - Fixed OnBroken status effects firing in the submarine editor when an item's condition is set to zero (for example, reactors exploding and breaking all the nearby walls). -- Fixed file number being added to the file extension of debug console log files ("file123.txt (2)" +- Fixed file number being added to the file extension of debug console log files ("file123.txt (2)" instead of "file123 (2).txt"). - Fixed battery positioning in charging docks. - Fixed crashing when ending a single player round while a character is outside the sub. @@ -4034,21 +4249,21 @@ instead of "file123 (2).txt"). - Fixed fire sounds persisting in menus. - Fixed the layout of the extra cargo menu in server settings. - Fixed depth charges disappearing from loaders when interacting them with both hand slots full. -- Fixed StatusEffects not being able to target item components. Caused doors to be impossible to weld -and most likely other issues with item StatusEffects as well. +- Fixed StatusEffects not being able to target item components. Caused doors to be impossible to weld +and most likely other issues with item StatusEffects as well. - Artifacts spawn in artifact holders again. - Fixes to "attempted to move pulljoint extremely far" errors which occasionally caused severe problems in syncing characters' positions. - Fixed a bug that occasionally caused monsters to spawn very close to the submarine in monster missions. -- Fixed servers occasionally starting the round multiple times when automatically starting the game via -autorestart or clients being ready. +- Fixed servers occasionally starting the round multiple times when automatically starting the game via +autorestart or clients being ready. - Fixed up-to-date content packages being reported as incompatible in the Steam workshop menu. - Changed the default radio chat hotkey to T. - Fixed the line of sight effect not working on ruins when looking at them from inside a sub. - Fixed fabricator allowing new items to be created when the output is not empty, resulting in wasted materials. - Fixed servers reporting incorrect player counts in the server list. - Fixed order messages not being visible in single player if the character issuing the order has no headset. -- Fixed riot shields retaining their pushing ability even when the user is stunned or unconscious. +- Fixed riot shields retaining their pushing ability even when the user is stunned or unconscious. - Fixed rubber ducks not floating like a good duck should. - Prevent locations from being generated too close to each other in the campaign map. - Fixed battery and supercapacitor charges not staying in sync between the server and clients. @@ -4056,7 +4271,7 @@ autorestart or clients being ready. - Fixed non-downloaded workshop items showing zero as the file size. - Fixed spectate button staying disabled if starting a round fails (due to a missing sub file for example). - Fixed crashing when teleporting characters from a submarine to ruins in multiplayer. -- Fixed automatic temperature control setting turbine output above 100 if the power consumption is higher +- Fixed automatic temperature control setting turbine output above 100 if the power consumption is higher than what the reactor can generate. Caused "failed to write an event for the entity" errors in multiplayer. - Fixed AI characters attempting to treat dead characters. @@ -4078,7 +4293,7 @@ with fires inside them. - Fixed the husk appendage not appearing on huskified humans. - Fixed order/report messages being flagged as spam way too easily, causing frequent spam kicks. - Fixed sliders buttons being invisible while pressed in device interfaces. -- Fixed an item being spawned in the submarine editor when selecting an item from the menu while another +- Fixed an item being spawned in the submarine editor when selecting an item from the menu while another one is already selected. - Fixed submarine colliders not taking into account the body offsets of the wall structures, causing some items outside the submarine's walls to be impossible to interact with (the most noticeable being the @@ -4094,39 +4309,39 @@ button that opens Orca's airlock from the outside). v0.8.9.1 (closed alpha) --------------------------------------------------------------------------------------------------------- -Too many changes to list here. :) We've been working on this update for a year now, with a team of about +Too many changes to list here. :) We've been working on this update for a year now, with a team of about a dozen people (as opposed to a couple of devs working on it on their free time). -Almost every aspect of the game has been improved to some extent - some polished a bit, some went through +Almost every aspect of the game has been improved to some extent - some polished a bit, some went through a more major overhaul and many things are completely new. This is by no means a complete list, but here's some of the new things: - A full graphics overhaul: almost all of the sprites has been polished or completely remade. - Improved random event system that tries to keep the overall difficulty of the game at certain level, -delaying additional monster spawns if there's already lots of things going on, or spawning more when +delaying additional monster spawns if there's already lots of things going on, or spawning more when there's a more quiet moment. - Improved difficulty system: now the difficulty level has a much more noticeable effect on the gameplay. - General difficulty balancing all across the board: we've tried to make the difficulty curve more approachable to new players while still keeping things challenging for more experienced players on higher difficulty levels. - More varied levels, environmental hazards. -- A new more detailed health system with things such as limb-specific injuries, addictions, overdoses, +- A new more detailed health system with things such as limb-specific injuries, addictions, overdoses, mental issues... The system is also highly moddable, and makes it much easier to implement things such as hunger mechanics, more varied poisons or stat-boosting items. - Completely redesigned in-game HUD (the inventory, crew command interface, chat, etc). - Redesigned crafting system. - Minerals scattered across the level (can be used for crafting). -- A command/report system that can be used to communicate with your crew more effectively (in both single +- A command/report system that can be used to communicate with your crew more effectively (in both single player and multiplayer). - Tons of additions to alien ruins (traps, puzzles, non-flooded rooms). - Improved AI (both the crew AI and the enemy AIs). -- NPC dialog (including random chatter and context-specific lines that make it easier to keep track of +- NPC dialog (including random chatter and context-specific lines that make it easier to keep track of what the crew is doing). - Most of the device interfaces have been redesigned to make them easier to use (and nicer to look at!). - Many additions to the campaign mode (still a work in progress though). -- Overhauled the skill system: now every character can generally do anything (repair devices, fabricate +- Overhauled the skill system: now every character can generally do anything (repair devices, fabricate new items, apply medical treatments), but characters with higher skill levels will do things more efficiently. -- Skill progression in the campaign mode: characters' skills gradually increase, making them more valuable with +- Skill progression in the campaign mode: characters' skills gradually increase, making them more valuable with each completed round. - New music composed specifically for the game. - Overhauled audio. @@ -4149,9 +4364,9 @@ v0.8.2.3 - Fixed a bunch of bugs that were causing "attempted to apply an invalid force/impulse to a physics body" errors. - Fixed a bunch of bugs that were causing "attempted to move a pulljoint extremely far" errors. -- Fixed DebugConsole selecting non-command lines if up/down is pressed when there are no commands in the -console. -- Fixed inventory syncing not working on the controlled character's inventory if the character is +- Fixed DebugConsole selecting non-command lines if up/down is pressed when there are no commands in the +console. +- Fixed inventory syncing not working on the controlled character's inventory if the character is unconscious or wearing handcuffs. - Verify that the launched exe belongs to the currently selected content package when starting up the game. - Fixed console messages that have been created before initializing the debug console not being present @@ -4170,18 +4385,18 @@ causing the camera not to follow the character and preventing the player from gi - Fixed a few ragdoll animation bugs that caused "attempted to move pulljoint anchor extremely far" errors. - Fixed AI characters (most often mantises) being able to attack through walls. - Fixed alien ruins occasionally overlapping with each other or being above the upper boundary of the level. -- Docking ports automatically stretch the hulls between them to cover the area between the docked subs. -Otherwise there may be areas uncovered by hulls if the docking port is positioned slightly outside the -extents of the submarine's hulls, causing characters to implode or get thrown back when they try to pass +- Docking ports automatically stretch the hulls between them to cover the area between the docked subs. +Otherwise there may be areas uncovered by hulls if the docking port is positioned slightly outside the +extents of the submarine's hulls, causing characters to implode or get thrown back when they try to pass from sub to another. -- Fixed client-side docking ports creating duplicate bodies on doors, causing characters to collide with +- Fixed client-side docking ports creating duplicate bodies on doors, causing characters to collide with an invisible door when trying to move between docked subs (until the server forces them through it). -- Fixed characters occasionally getting teleported outside the sub for a few frames when moving between +- Fixed characters occasionally getting teleported outside the sub for a few frames when moving between docked subs. - Fixed status effects that deplete oxygen affecting characters that don't need air to breathe. - More error logging to diagnose syncing errors. - Melee weapons can only hit one character per swing (makes stun batons and medical syringes less OP). -- Option to make RegEx component only send a signal when it receives a signal (not continuously according +- Option to make RegEx component only send a signal when it receives a signal (not continuously according to the last received signal). - Added FalseOutput property to RegEx components. @@ -4194,10 +4409,10 @@ v0.8.2.1 another. - Fixed dragged characters staying floating mid-air after they've been dragged up/down ladders. - Attempt to fix items occasionally dropping client-side when moving them from an inventory to another. -- Fixed incorrect rotation of welding tools and other 2-handed items that are held in one hand when not +- Fixed incorrect rotation of welding tools and other 2-handed items that are held in one hand when not aiming. The items were rotated according to the left hand, but positioned on the right hand. - Spaces and exclamation marks are allowed in client names by default. -- Hitscan weapons like the revolver can hit targets outside the sub when firing from the inside and +- Hitscan weapons like the revolver can hit targets outside the sub when firing from the inside and vice versa. - Fixed crashing when attempting to clone linked submarines in the sub editor. - Fixed crashing when attempting to clone items with non-default required items. @@ -4211,56 +4426,56 @@ v0.8.2.0 --------------------------------------------------------------------------------------------------------- Networking additions: - - Added a server setting for selecting which symbols are allowed in client names (see + - Added a server setting for selecting which symbols are allowed in client names (see AllowedClientNameChars in the server settings file). - Custom servers can modify all editable item properties mid-round, not just in-game editable ones. - Clients can be given access to server logs. - Respawn durations can be changed mid-round. - Servers have the option to disable the disguise feature. - Increased midround syncing timeout. - + Misc changes: - Levels are mirrored when travelling through the backwards in the campaign mode. - Added colliders to railguns (so they cannot go through walls or enemy subs anymore). - - Melee weapons can hit multiple targets on one swing. Fixes weapons occasionally not hitting + - Melee weapons can hit multiple targets on one swing. Fixes weapons occasionally not hitting the target in tight spaces due to touching the ceiling/walls first. - - The voltage required for a PowerTransfer item to take damage and the probability for a fire can be + - The voltage required for a PowerTransfer item to take damage and the probability for a fire can be configured in the item xmls. - Docking ports and hatches aren't damaged by excess voltage. - Added more color variants of wires. - Characters point the harpoon gun down when not aiming. - Added parameter autocompletion to the kill command. - - Added a property that can be used to lock connection panels but still keep the panel rewireable + - Added a property that can be used to lock connection panels but still keep the panel rewireable in the submarine editor. - Items outside the sub cannot be deattached from walls. - - Fabricators show the list of required items even if the character does not have the skills to craft + - Fabricators show the list of required items even if the character does not have the skills to craft the item. Networking bugfixes: - - Fixed file transfers failing if the client disconnects during an active transfer, rejoins and + - Fixed file transfers failing if the client disconnects during an active transfer, rejoins and attempts to receive the same file. - - Fixed a bug in door syncing that caused the door states to differ between the server and clients + - Fixed a bug in door syncing that caused the door states to differ between the server and clients in some subs with more complex door wiring setups. - - Fixed clients being able to spam kick votes (duplicate votes were not counted but caused unnecessary + - Fixed clients being able to spam kick votes (duplicate votes were not counted but caused unnecessary chat messages to be sent). - - Fixed item conditions occasionally not matching exactly between the server and clients, causing - issues such as not clients not being able to fabricate items due to the condition being slightly + - Fixed item conditions occasionally not matching exactly between the server and clients, causing + issues such as not clients not being able to fabricate items due to the condition being slightly below the minimum condition at their end. Bugfixes: - Added a workaround to a MonoGame bug that makes the screen turn white when alt-tabbing out of fullscreen. - Fixed docking ports flooding for no reason in some custom subs. - Fixed LightComponents staying active on broken items. - - Fixed railguns and depth charge tubes being directly usable by characters (= they could be launched + - Fixed railguns and depth charge tubes being directly usable by characters (= they could be launched simply by selecting them and left clicking, without the need to use a railgun controller). - Fixed items salvaged from ruins not being saved in the campaign mode. - - Fixed LOS effect being brighter than the ambient light in some of the darker levels, causing + - Fixed LOS effect being brighter than the ambient light in some of the darker levels, causing the player to see obstructed areas better than unobstructed ones. - Fixed severed limbs staying disabled when a dismembered character is revived using console commands. - Fixed characters holding non-aimable two-handed items such as railgun shells in one hand when aiming. - Fixed stereo sounds not being loaded correctly. - Fixed modified sprite colors not working correctly on worn items. - - Fixed modified maximum recharge speeds of PowerContainers resetting to the default value after + - Fixed modified maximum recharge speeds of PowerContainers resetting to the default value after saving and reloading. - Fixed handcuffed players being able to perform CPR and grab/drag bodies. - Fixed diving suit's damage modifiers being bypassed if the character gets hit in the waist. @@ -4271,13 +4486,13 @@ v0.8.1.12 --------------------------------------------------------------------------------------------------------- - Fixed connectionpanel syncing (wires dropping client-side during rewiring). -- Characters stay alive for 30 seconds after a client disconnects, and if the client rejoins during that -time they regain control of the character. +- Characters stay alive for 30 seconds after a client disconnects, and if the client rejoins during that +time they regain control of the character. - Changing the sprite color of an item also affects the color when the item is being worn. - Cloned items keep the value of the "required items" field of the original item. - Fixed crashes caused by gaps that are not connected to anything. - Removed the unused and non-functional monitor item. -- Made medical and toxic cabinets waterproof (= potassium and other water-sensitive items inside them +- Made medical and toxic cabinets waterproof (= potassium and other water-sensitive items inside them are not affected by water). - Fixed docking ports causing flooding in some custom subs. - Fixed medical items with an immediate effect (such as calyxanide) not working when a player uses them @@ -4298,22 +4513,22 @@ Networking fixes: clients to get kicked due to desync. - Fixed client-side error messages when respawning without a respawn shuttle. - Fixed some issues in inventory and connection panel syncing when joining mid-round. - - Fixed fabricated items always appearing to be in full condition client-side (e.g. oxygen tanks which + - Fixed fabricated items always appearing to be in full condition client-side (e.g. oxygen tanks which should be empty after being fabricated). - - Fixed attachable items dropping on the ground client-side when deattaching them (but still staying + - Fixed attachable items dropping on the ground client-side when deattaching them (but still staying in the inventory of the character detaching them). - Fixed items occasionally dropping instead of being moved to another inventory client-side. - The "traitorlist" command is usable by clients who have the permission to use it. Misc bugfixes: - - Fixed characters occasionally going inside/through obstacle when leaving a submarine that's right + - Fixed characters occasionally going inside/through obstacle when leaving a submarine that's right next to another submarine or a level wall. - More physics error checks and logging. - Railgun controllers can be used over wifi components. - Characters attach items to the walls at the position of their hand, not at the center of the body. - Wifi components can't communicate with the enemy sub in combat missions. - - Fixed the previously selected location staying selected but start button staying disabled when - returning to the lobby screen in the single player campaign. Made it impossible to progress without + - Fixed the previously selected location staying selected but start button staying disabled when + returning to the lobby screen in the single player campaign. Made it impossible to progress without restarting if there were no other selectable locations. - Fixed holdable components reverting their RequiredItems back to the prefab values during loading. - Fixed wall-attached sections of a wire not rendering when the item is being rewired outside the sub. @@ -4323,11 +4538,11 @@ Misc bugfixes: v0.8.1.10 --------------------------------------------------------------------------------------------------------- -- Fixed bugs in wall hole creation logic and docking port syncing which caused entity ID mismatches and +- Fixed bugs in wall hole creation logic and docking port syncing which caused entity ID mismatches and "unknown object header" errors. - Fixed errors when attempting to buy too many items of a given type to fit in one container. - Fixed crashing when attempting to buy items that don't spawn in a container. -- Fixed crashing when attempting to generate hulls with the "autohull" command when there are no walls +- Fixed crashing when attempting to generate hulls with the "autohull" command when there are no walls or doors in the sub. - Fixed docking ports creating duplicate hulls and gaps during loading. - Fixed missions resetting to the initial ones when loading a campaign. @@ -4340,14 +4555,14 @@ v0.8.1.9 --------------------------------------------------------------------------------------------------------- - Fixed a bug in docking port syncing that caused entity ID mismatches and "unknown object header" errors. -- Fixed a bug that occasionally caused entity ID mismatches and "unknown object header" errors when +- Fixed a bug that occasionally caused entity ID mismatches and "unknown object header" errors when a respawn shuttle got damaged before returning back to the starting location. - Fixed error messages when attempting to use the console commands "\" or "\n". - Fixed clients not syncing the position of their controlled character with the server when dead/unconscious. - Fixed swimming ragdolls "dropping down" if the server freezes them due to a connection error. - Fixed excessive "attempted to apply invalid velocity to a physics body" console errors. - Fixed crashing when selecting a level seed that has no background portrait defined. -- Fixed crashing if the player clicks yes on the "download sub from the server" prompt after +- Fixed crashing if the player clicks yes on the "download sub from the server" prompt after returning to the main menu. --------------------------------------------------------------------------------------------------------- @@ -4384,7 +4599,7 @@ them (despite the server allowing voting if the client has had a character earli use the position of the client's cursor. - Fixed crashing if a wire is used by a statuseffect (for example if a detonator tries to trigger a wire contained inside it). -- Fixed GameAnalytics being stopped if the dedicated server is restarted with the "restart" console command.- +- Fixed GameAnalytics being stopped if the dedicated server is restarted with the "restart" console command.- - Fixed wiring items outside the submarine. - Fixed chatbox discarding the second chat message instead of the first one when the maximum number of chat messages is reached. @@ -4405,7 +4620,7 @@ select whether you want to send the information or not. - Devices outside the submarine can be rewired in-game (not just in the sub editor). - Fixed a crash caused by vision obstruction logic. - Fixed clients being unable to give non-permanent or range bans. -- Clients are allowed to vote to end the round if they have spawned at some point during the round, +- Clients are allowed to vote to end the round if they have spawned at some point during the round, even if the character they controlled doesn't exist anymore. - Dedicated servers can give clients the permission to use console commands that aren't available in for dedicated server (e.g. los, lights, control) @@ -4414,7 +4629,7 @@ for dedicated server (e.g. los, lights, control) the command instead of the host's character. - Spawnitem can be used to spawn items in the inventory of a specific character. - Fixed explosions with an EMP value only damaging reactors (when they should only ignore reactors). -- Fire can only explode oxygen tanks that are >25% full (otherwise the condition of the tank just drops +- Fire can only explode oxygen tanks that are >25% full (otherwise the condition of the tank just drops to 0). Prevents infinite explosions when an oxygen generator is on fire with oxygen tanks inside. - Fixed projectiles with a damage range of 0 not applying their structuredamage value to structures. - Items with a physics body can be used as pumps, so now it's possible to make portable items that remove @@ -4434,12 +4649,12 @@ v0.8.1.3 --------------------------------------------------------------------------------------------------------- - Fixed server-side crashes during job assignment if a client hasn't sent any job preferences. -- Fixed crashing if the selected respawn shuttle doesn't have a navigation terminal or any other item +- Fixed crashing if the selected respawn shuttle doesn't have a navigation terminal or any other item with a Steering component. -- Fixed InWater status effects triggering when an item is fabricated, causing issues such as +- Fixed InWater status effects triggering when an item is fabricated, causing issues such as water-sensitive items to breaking/exploding immediately after being fabricated. - Fixed motion sensors sending out signals even if the output is set to nothing. -- Fixed crashing when a round starts if the sub has been saved while a fabricator was running. +- Fixed crashing when a round starts if the sub has been saved while a fabricator was running. - Fixed explosives not detonating inside railgun shells. - Fixed characters spawning inside the respawn shuttle if no suitable spawnpoint is found inside the main submarine. @@ -4463,7 +4678,7 @@ v0.8.1.2 v0.8.1.1 --------------------------------------------------------------------------------------------------------- -- Fixed explosives going off when a character holds them in their hand and left clicks, causing a crash +- Fixed explosives going off when a character holds them in their hand and left clicks, causing a crash if done in the submarine editor. --------------------------------------------------------------------------------------------------------- @@ -4472,17 +4687,17 @@ v0.8.1.0 Items: - Added searchlights. - - Explosives, chemicals and medical items disappear when their condition falls to 0 + - Explosives, chemicals and medical items disappear when their condition falls to 0 (i.e. when they're fully used). - Railguns cannot be fired when not being aimed. - - Removed the need for batteries in diving suits. The light stays on as long as the suit is worn + - Removed the need for batteries in diving suits. The light stays on as long as the suit is worn by a living character. - Junction boxes only take damage underwater when they're powered up. Bugfixes: - - Fixed a bug that occasionally caused some characters to not be removed at the end of a round, causing - various bugs and crashes on successive rounds (the most common ones being server-side crashes and - constant "attempted to access a potentially removed ragdoll" console errors). + - Fixed a bug that occasionally caused some characters to not be removed at the end of a round, causing + various bugs and crashes on successive rounds (the most common ones being server-side crashes and + constant "attempted to access a potentially removed ragdoll" console errors). - Fixed camera shake continuing indefinitely if a character falls unconscious due to impact damage. - Fixed item removal by right clicking not being synced with clients. - Fixed being able to gain karma by welding fixed walls. @@ -4494,14 +4709,14 @@ Bugfixes: - Fixed the EndRound music clip occasionally looping forever after a round ends. - Fixed player-controlled creatures being able to damage themselves. - Fixed repair tools causing damage to the user regardless of the character's skills. - - Attempt to fix characters occasionally getting launched out of the sub at lightspeed when the sub + - Attempt to fix characters occasionally getting launched out of the sub at lightspeed when the sub crashes into something. - Fixed StatusEffects not working on child ItemComponents. - Wearables apply OnWearing StatusEffects in all the components of an item, not just the Wearable component. - Fixed Equals/NotEquals conditional comparisons. Misc additions: - - Added console commands for giving the clients ranks, showing their current permissions and + - Added console commands for giving the clients ranks, showing their current permissions and giving/revoking the permission to use specific console commands. - Option to set an automatic ban duration for vote kicked players. - Option to log debug console output into a text file. @@ -4524,7 +4739,7 @@ when entities were removed mid-round. - Fixed clients getting desynced if the server ends a campaign and starts a new one. - Fixed the "campaign view" button staying visible in the server lobby after the campaign has ended. - Fixed message boxes appearing behind the campaign setup menu in the server lobby. -- Fixed round summaries always showing the game over text in multiplayer if the submarine didn't progress +- Fixed round summaries always showing the game over text in multiplayer if the submarine didn't progress to the next location. - Fixed missing spark particles when welding/cutting a wall. - Fixed plasma cutters not affecting hatches, alien doors or duct blocks. @@ -4534,20 +4749,20 @@ instead of a limb. - Fixed crashing when attempting to perform CPR on a headless character or AS a headless character. - Fixed attachable items being deattachable with the select key instead of the use key. - Fixed clients being unable to open doors with crowbars. -- Fixed items attached mid-round by other clients or the host being impossible to interact with +- Fixed items attached mid-round by other clients or the host being impossible to interact with and occasionally being attached to an incorrect position. - Fixed batteries not getting recharged in charging docks. - Fixed monsters being able to spawn under the ocean floor in levels where the floor is high up. - Fixed effects of the medical items not being stackable, meaning that successive usages of a medicine did not have an effect until the effect of the first dose has worn off. -- Fixed items in itemcontainers (e.g. shells in railgun loaders, batteries in recharging docks) always +- Fixed items in itemcontainers (e.g. shells in railgun loaders, batteries in recharging docks) always being rendered with the default sprite color. - Fixed crashing when clicking the "refreshing server list" text in the server list menu. - Fixed dedicated servers not resetting votes when a round ends. - Fixed interaction areas of some items being incorrect in the dedicated server. - Fixed the removal of items that get deleted after being used not being synced. -- Ladder waypoint generation fix: waypoints are not just placed at the top and bottom of the ladders, -but above every platform along the ladders (-> waypoints work correctly on ladders spanning through +- Ladder waypoint generation fix: waypoints are not just placed at the top and bottom of the ladders, +but above every platform along the ladders (-> waypoints work correctly on ladders spanning through multiple floors). --------------------------------------------------------------------------------------------------------- @@ -4579,13 +4794,13 @@ v0.8.0.2 - Sound configuration files are included in content packages. - Fixed "file not found" errors when a character is wearing footwear with no configured footstep sounds. - MODDERS PLEASE NOTE: hit sounds on limbs and wearable items must now use tags instead of direct paths -to the sound file. New sound files and tags can be added by editing the sound configuration files. +to the sound file. New sound files and tags can be added by editing the sound configuration files. --------------------------------------------------------------------------------------------------------- v0.8.0.1 --------------------------------------------------------------------------------------------------------- -- Fixed crashing when creating items with a PowerTransfer component mid-round (e.g. when fabricating +- Fixed crashing when creating items with a PowerTransfer component mid-round (e.g. when fabricating a relay component) - Fixed dead/spectator chatting in multiplayer. - Chatboxes can be deselected with the chat hotkey again. @@ -4604,9 +4819,9 @@ v0.8.0.0 - Made ambient lighting much darker and added a subtle glow around the player. - Made partially damaged walls leak much more slowly. -- Nerfed wall damage. Crawlers, mantises, threshers and coelanths now take much more time to tear through +- Nerfed wall damage. Crawlers, mantises, threshers and coelanths now take much more time to tear through the hull and collisions with the level cause less damage. -- Moved a bunch of hard-coded texts to an xml file "Content/Texts.xml". Full translation support is still +- Moved a bunch of hard-coded texts to an xml file "Content/Texts.xml". Full translation support is still in progress, but now it should be possible to translate most of the in-game texts without recompiling the game. - Optimized rendering. - More accurate endworm attack hit detection. @@ -4651,7 +4866,7 @@ Items: - Made more items craftable and deconstructable. - Added sprites for broken doors, hatches and junction boxes. - Health Scanner HUD displays causes of death. - - Health Scanner HUD only shows the status of the visible character that the cursor is closest to + - Health Scanner HUD only shows the status of the visible character that the cursor is closest to prevent multiple characters from cluttering the screen - Added "smallitem" tag to revolver rounds (can be placed in cabinets and pockets now). - Option to determine which types of targets a projectile can stick to. @@ -4660,29 +4875,29 @@ Items: - Characters can hold flashlights in their mouths. - Added an inventory slot for ID cards. - If a character is wearing an item that obscures their face, the game will either hide their name or - show the name that's in the ID card the character is wearing. + show the name that's in the ID card the character is wearing. - The owner of an ID card is stated in the description of the card. - - Fabricating items requires the ingredients to have a specific minimum condition (can't use + - Fabricating items requires the ingredients to have a specific minimum condition (can't use already-used consumables to craft something). - Deconstructing an item that's not in a full condition may prevent some deconstruction products from appearing. - Optimized electricity/signal logic. - Grenades can be triggered by detonators. - Bought cargo spawns in containers instead of being scattered across the floor. - - Players can't use other items when a railgun controller is selected. Prevents accidentally firing + - Players can't use other items when a railgun controller is selected. Prevents accidentally firing weapons or hitting people with something while using the railguns. - Item editing menus display color values as 0-255 instead of 0-1. - - Reactor temperature has to be critical for 30 seconds before the reactor explodes, giving the crew - more time to deal with griefers or incompetent reactor operators. The reactors also have an output + - Reactor temperature has to be critical for 30 seconds before the reactor explodes, giving the crew + more time to deal with griefers or incompetent reactor operators. The reactors also have an output connection that sends out a signal when the temperature is critical. Multiplayer additions: - - Added an optional "karma system". Harming other players, damaging structures and blowing up the reactor - reduces karma, while repairing things causes it to increase. A too low karma level prevents the players + - Added an optional "karma system". Harming other players, damaging structures and blowing up the reactor + reduces karma, while repairing things causes it to increase. A too low karma level prevents the players from choosing specific jobs - Clients can be given permission to use specific console commands. - Client permission presets: the clients can be assigned as moderators or admins which have specific pre-configured permissions. New presets can be added by editing Data/permissionpresets.xml. - - Option to have more than one traitor. Traitors also now get a set of code words that can be used to + - Option to have more than one traitor. Traitors also now get a set of code words that can be used to secretly identify other traitors. - Added an option to make players spawn directly in the main submarine instead of the respawn shuttle. - Keybinds are disabled when the chatbox is active. Now it's possible to use normal letter/number keys @@ -4693,24 +4908,24 @@ Multiplayer additions: - Cutting and repairing walls is included in server logs. Multiplayer bugfixes: - - Fixed clients getting disconnected due to desync when a new monster is spawned mid-round by + - Fixed clients getting disconnected due to desync when a new monster is spawned mid-round by a repeating monster event. - Fixed modified clients being able to chat while unconscious due to the lack of server-side checks. - Fixed modified clients being able to disconnect locked wires due to the lack of server-side checks. - Fixed chat messages being assigned to the wrong sender when their bodies have been eaten. - Fixed crashing when setting a server filter while the game is refreshing the server list. - - Fixed wires not being dropped server-side when a player drops a connected wire without dragging it + - Fixed wires not being dropped server-side when a player drops a connected wire without dragging it to their inventory first. - Fixed 5th server being impossible to select in the server list. - Improvements to character position syncing. - Re-enabled logic for preventing players from using visually similar names. - - Fixed client-side null exception when the client is in the lobby and a round ends with the mission + - Fixed client-side null exception when the client is in the lobby and a round ends with the mission successfully completed. - Fixed clients being able to votekick/kick/ban themselves in the server lobby. - Fixed "selected mode" and "mission type" settings not being saved. - Level seed randomization can be toggled on and off via the debug console. - Fixed dedicated server not randomizing the submarine or game mode even if randomization is enabled. - - Lighting is forced back on when a client starts a round (-> can't disable lighting by using the + - Lighting is forced back on when a client starts a round (-> can't disable lighting by using the console command before joining a server). Bugfixes: @@ -4739,11 +4954,11 @@ Bugfixes: - Fixed being able to use a wrench on multiple items at the same time. - Fixed double-clicking items in corpses putting them in their hands instead of your own inventory. - Fixed some level seeds generating a tiny enclosed cave that makes it impossible to reach the destination. - - Fixed opened and broken doors being ignored during waypoint generation, causing waypoint connections + - Fixed opened and broken doors being ignored during waypoint generation, causing waypoint connections to go through doors which prevented AI characters from opening them. - Fixed modified structure colors not being cloned. - Fixed modified wall colors only being visible in the submarine editor. - - Fixed items being dropped when attempting to place them in an itemcontainer slot that's on a normal + - Fixed items being dropped when attempting to place them in an itemcontainer slot that's on a normal inventory slot. - Fixed stack overflow exceptions caused by signal loops between junction boxes. - Fixed submarine editor crashing when attempting to use illegal characters in the filename. @@ -4766,7 +4981,7 @@ v0.7.0.1 - Removed serverconfig.xml (the dedicated server now uses the same config file as the normal game). - Updated the vanilla content package to version 0.7. - Fixed entity linking in the submarine editor. -- Fixed railgun HUD crashing the game if the railgun is linked to an item that does not have an +- Fixed railgun HUD crashing the game if the railgun is linked to an item that does not have an ItemContainer component (i.e. any item that can't contain other items). - Fixed exceptions when the player dies in the tutorial. - Fixed the start popup saying the host is the target if the host has been selected as the traitor. @@ -4806,7 +5021,7 @@ Particles: Bugfixes: - Fixed the "DXGI_ERROR_DEVICE_REMOVED" crashes on specific GPUs when the loading reaches 80%. - Fixed crashes when projectiles stuck to items on dedicated server. - - Fixed a bunch of bugs that caused crashes when a character was removed mid-round (for example when + - Fixed a bunch of bugs that caused crashes when a character was removed mid-round (for example when a character turns into a husk). - Fixed a bug that occasionally caused swimming creatures to flip around constantly. - Fixed a bug that caused creatures to be able to sever limb joints that shouldn't be possible to sever, @@ -4819,7 +5034,7 @@ Bugfixes: - Fixed AI characters never making any sounds in multiplayer. - Fixed inability to rebind keys to Mouse2 via the settings menu. - Fixed destroyed doors being impossible to repair. - - Fixed creatures seeking towards an incorrect position when trying to eat something (causing larger + - Fixed creatures seeking towards an incorrect position when trying to eat something (causing larger creatures like threshers and coelanths to swim around the target without ever reaching it). Items: @@ -4830,7 +5045,7 @@ Items: - Added sprites for destroyed doors/hatches. - Added a HUD that shows the charge of the supercapacitors and the amount of shells left when using a railgun. - - Inventory slots are be highlighted even if the cursor is within the empty space between them. Now + - Inventory slots are be highlighted even if the cursor is within the empty space between them. Now items can't be accidentally dropped by releasing the mouse button between the slots. - Sounds for picking up and dropping items. - Medical scanner displays husk infections. @@ -4838,7 +5053,7 @@ Items: - Optimizations and minor visual changes to the sonar display. - Support for hitscan projectiles. - Fixed ranged weapons launching projectiles with an incorrect rotation. - - The spread of ranged weapons can be adjusted (separate values for normal spread and spread when + - The spread of ranged weapons can be adjusted (separate values for normal spread and spread when the item is being used by a character with an inadequate skill level). - Heavier harpoon gun recoil and impulse when the spear hits something. @@ -4851,7 +5066,7 @@ Items: Misc: - Flowing water pushes characters around much more heavily. - Warning texts when water pressure is increasing to dangerous levels and when running out of oxygen. - - Made the damage range of limb attacks configurable (instead of having it always be half of the distance + - Made the damage range of limb attacks configurable (instead of having it always be half of the distance at which the attack activates) and tweaked the damage ranges of all the creature attacks. - Option to filter the server list based on a bunch of criteria. - Added a radio chat hotkey. @@ -4867,18 +5082,18 @@ v0.6.1.4 - Fixed reactors staying operational after the fuel rods run out. - Fixed spawning items at the cursor via the debug console. - Fixed job assignment logic causing crashes if the maximum amount of players per job has been reached -on the top 3 preferences of a client (which can happen on modded servers that have several jobs with a +on the top 3 preferences of a client (which can happen on modded servers that have several jobs with a player limit). - Fixed errors during job assignment if a client or the host is controlling a non-human character. - The server log menu remembers the state of the filters when toggling it. - Fixed level generation errors in some specific seeds that caused the game to create ruin walls with a negative width/height (example seed: cBLgZ2im). -- Fixed a submarine position syncing issue that caused erratic physics behavior on some specific +- Fixed a submarine position syncing issue that caused erratic physics behavior on some specific multi-part subs, because the game only moved one part of the sub and the parts docked to it, not taking into account that more parts may be docked to the docked parts. - Handcuffed AI characters can't climb ladders. - Fixed crashing when a huskified human is killed by an explosion. -- Added some GPU info to the crash reports and extra debug logging to hopefully diagnose the +- Added some GPU info to the crash reports and extra debug logging to hopefully diagnose the SharpDXExceptions during startup. --------------------------------------------------------------------------------------------------------- @@ -4887,7 +5102,7 @@ v0.6.1.3 - Fixed hulls not being rendered in the submarine editor. - Crouching is synced between the server and the clients. -- Plasma cutters and welding tools ignore platforms and stairs - placing a platform on a wall doesn't +- Plasma cutters and welding tools ignore platforms and stairs - placing a platform on a wall doesn't prevent welding/cutting the wall anymore. - Using the SetClientCharacter console command forces the client's line of sight effect back on. - Fixed null reference exceptions when syncing docking ports that haven't been docked to anything. @@ -4901,7 +5116,7 @@ v0.6.1.2 - Dedicated servers can use autorestart. - Fixed dedicated servers ignoring armor. - Fixed console messages not appearing in the crash reports if the game crashes during loading. -- Attachable items (buttons, electrical components, etc) are automatically attached to walls when placed +- Attachable items (buttons, electrical components, etc) are automatically attached to walls when placed in the submarine editor. - Fixed crashing if no matching character is found when using the SetClientCharacter console command. - Fixed wires disconnecting from a connection panel when a player moves any of the wires. @@ -4932,13 +5147,13 @@ Multiplayer: - Job preferences don't reset when quitting the game. - Added MessageBox chat message type. Allows custom servers to display custom message boxes to the clients. - Logging when a character throws an item. - - Logging which items are contained inside items characters use on themselves (e.g. which meds are + - Logging which items are contained inside items characters use on themselves (e.g. which meds are inside a medical syringe). - Logging which type of projectile was launched from a railgun and which items were contained inside it. - - More descriptive wiring logging: the logs don't list all the wires in a connection panel but only + - More descriptive wiring logging: the logs don't list all the wires in a connection panel but only the changes players do to the wiring. -Monsters: +Monsters: - Some creatures can hunt for smaller creatures (including humans) and eat them. - Tweaked enemy AI to make their attacks less likely to miss. - Some creatures flee when their health decreases below a specific threshold. @@ -4950,26 +5165,26 @@ Monsters: - The camera zooms further out when controlling a large non-humanoid character. Misc: - - Improved item interaction logic: highlighting items is more precise, with items directly under + - Improved item interaction logic: highlighting items is more precise, with items directly under the cursor taking priority. - Characters can be dismembered by creatures and explosions. - New blood particles. - Blood, explosion and fire decals. - - Added an artifact that attracts creatures. - - Detached buttons and electrical components can be picked up just like any other item, instead of + - Added an artifact that attracts creatures. + - Detached buttons and electrical components can be picked up just like any other item, instead of having to use a wrench and wait for the item to "detach". - Wires can't be connected to detached items. - Debug commands can be autocompleted using tab. - Added a debug command for creating explosions. - -Bugfixes: + +Bugfixes: - Fixed "loading was interrupted due to an error" crashes on startup. - Fixed "destination array was not long enough" errors in AddToGUIUpdateList. - Fixed error messages when a character gets stunned for over 60 seconds in multiplayer. - Characters don't consume oxygen from rooms when wearing a diving mask or a diving suit. - Fixed occasionally seeing through walls when swimming outside a submarine. - Fixed crashes during map generation caused by very large wall cells near the entrance of the level. - - When highlighting a wire in a connection panel, the physical wire and the items connected to it are + - When highlighting a wire in a connection panel, the physical wire and the items connected to it are highlighted. - Fixed crashing when selecting a sonar monitor in a submarine with no hulls. - Fixed submarine/shuttle lists occasionally appearing empty after joining a server. @@ -4986,7 +5201,7 @@ v0.6.0.2 - Fixed a bug that caused non-interactable checkboxes to always appear unchecked. - Skill level syncing fix: the syncing isn't dependent on the order of the characters skills anymore. - IP addresses are included in all login error messages and the errors are also logged to the debug console. -- Servers end rounds if all players are either dead or unconscious when autorestart is on (instead of +- Servers end rounds if all players are either dead or unconscious when autorestart is on (instead of waiting for all players to die) - Fixed nuclear shells and depth charges exploding immediately when launched. - Fixed a bug that prevented any broken items from being repaired in the single player. @@ -4999,8 +5214,8 @@ v0.6.0.1 - Readded spam filter. - Servers log the automatic temperature control setting of nuclear reactors. -- If a client fails to start a round (due to a missing sub file or an error, for example), their character -is automatically killed. This prevents situations where a team can't win a combat mission due to a +- If a client fails to start a round (due to a missing sub file or an error, for example), their character +is automatically killed. This prevents situations where a team can't win a combat mission due to a disabled, invisible character in the opposing team. - Fixed clients occasionally displaying the "crew has been defeated" message immediately after a combat mission starts. @@ -5033,7 +5248,7 @@ UI: - multi-line chat messages don't overlap Items: - - passive sonar: when not active, the sonar shows nearby sources of sound and a faint outline of the + - passive sonar: when not active, the sonar shows nearby sources of sound and a faint outline of the structures around them. Now it's much easier to monitor how much noise the submarine is making and to hide from enemies. - new sonar visuals @@ -5043,7 +5258,7 @@ Items: - buttons created in fabricators work now Submarine editor: - - items/structures that have been copy-pasted from another submarine don't disappear when saving and + - items/structures that have been copy-pasted from another submarine don't disappear when saving and loading the sub - fixed crashes when attempting to load a submarine with no walls - placing a resizable structure with a height/width of zero is not allowed @@ -5061,7 +5276,7 @@ Misc: - heal and revive commands can also be used on other characters than the controlled one - fixed fires occasionally causing incorrect sound clips to loop continuously - AI controlled crew members are better at avoiding hazards such as water and fire - - swimming animation fix: characters don't swim with their legs extended up over their shoulders + - swimming animation fix: characters don't swim with their legs extended up over their shoulders after a sharp turn --------------------------------------------------------------------------------------------------------- @@ -5106,7 +5321,7 @@ v0.5.4.3 - a new enemy - some new sound effects by Omniary - some structure-specific damage sounds -- the size of docked subs is taken into account when determining the spawn position of the sub (large +- the size of docked subs is taken into account when determining the spawn position of the sub (large multi-part subs shouldn't spawn inside walls anymore) - explosion damage is calculated based on the distance to the closest surface of a limb instead of the center position of the limb (i.e. large monsters can be damaged by smaller explosions) @@ -5126,13 +5341,13 @@ v0.5.4.2 --------------------------------------------------------------------------------------------------------- - fixed crashes when removing nodes from a wire (i.e. right clicking with a wire equipped) -- fixed inventory not being drawn in the correct position if switching to a character who's been +- fixed inventory not being drawn in the correct position if switching to a character who's been dragged/grabbed by some other character - fixed wires becoming disconnected when copypasting them - wire nodes can't be moved when connecting wires to a connection panel - fixed repeating crash messageboxes if the game fails to resolve a SharpDX exception on startup - fixed crashing when switching to wiring mode while editing some value of an item -- fixed keyboard focus staying in textboxes after the textbox has been hidden (for example, +- fixed keyboard focus staying in textboxes after the textbox has been hidden (for example, the input fields in the submarine saving prompt) - fixed error message spam if a docking port is linked to another port in the same sub - submarine lists in the editor, main menu and server menu are updated when new subs are saved/received @@ -5141,7 +5356,7 @@ the input fields in the submarine saving prompt) - the size of the docked subs is taken into account when generating the level - fixed autorestart timer not resetting at the clients' end if the server fails to start a shift and resets the timer -- docked subs are forced to correct positions during loading (subs won't get stuck inside each other +- docked subs are forced to correct positions during loading (subs won't get stuck inside each other even if the submarines are slightly overlapping in the editor) - all sounds are paused when switching to submarine editor @@ -5159,19 +5374,19 @@ Bugfixes: - fixed a bug that occasionally caused crashing when the game happens to generate a very small level Sub editor: - - structures/items that are behind something else can be selected using a listbox that appears - when hovering the cursor over them - - wires have to be selected by clicking before any of the points can be moved (makes it possible + - structures/items that are behind something else can be selected using a listbox that appears + when hovering the cursor over them + - wires have to be selected by clicking before any of the points can be moved (makes it possible to move the correct wire even if it's overlapping with other wires) - the selected wire is renderer over all structures - points can be added to wires by clicking while holding ctrl - disabled music Misc: - - some rendering optimization + - some rendering optimization - pathfinding and waypoint generation improvements - made mantises more aggressive - - water flows more slowly through partially damaged walls + - water flows more slowly through partially damaged walls --------------------------------------------------------------------------------------------------------- v0.5.4.0 @@ -5180,12 +5395,12 @@ v0.5.4.0 Submarine editor: - copy, paste and cut functionality - items/structures can be copied by holding ctrl while dragging - - it's possible to move a wire by moving both items it's connected to (without having to move each + - it's possible to move a wire by moving both items it's connected to (without having to move each individual point of the wire separately) - - "hull volume helper" which makes it easier to select a suitable ballast tank size and + - "hull volume helper" which makes it easier to select a suitable ballast tank size and NeutralBallastLevel setting in the navigation terminal - equipped items are removed when switching from wiring mode to character mode or vice versa - - no need to wait when deattaching items from the walls with a wrench + - no need to wait when deattaching items from the walls with a wrench Bugfixes: @@ -5193,18 +5408,18 @@ Bugfixes: - UI elements (buttons, textboxes, etc) can't be clicked through each other anymore - fixed a bug that caused crashes when deattaching items from walls - fixed a game-crashing particle bug - - fixed respawned characters getting assigned to a different team than the rest of the characters + - fixed respawned characters getting assigned to a different team than the rest of the characters (causing them to be displayed separately in the crew menu) - pathfinding/autopilot fixes Misc: - - server hosts can give players special privileges (kick, ban, end round) + - server hosts can give players special privileges (kick, ban, end round) - saving the contents of the server info box and the traitor setting - - changes to battery logic: they can now be used to cover the entire power consumption of the + - changes to battery logic: they can now be used to cover the entire power consumption of the electrical grid (assuming their maximum output is high enough) - - added "artifact holders" to alien ruins (which can also be used for turning artifacts into power + - added "artifact holders" to alien ruins (which can also be used for turning artifacts into power sources if installed in a sub) - - changes to character collider behavior: crouching changes the size of the collider and it's + - changes to character collider behavior: crouching changes the size of the collider and it's easier to step over small obstacles @@ -5224,7 +5439,7 @@ v0.5.3.3 - fixed a bug that caused crashes after a husk-infected player died - disabled the "zoom effect" when under pressure as a huskified human -- only a limited number of messages are kept in the debug console (prevents performance issues if large +- only a limited number of messages are kept in the debug console (prevents performance issues if large amounts of messages are added) - some item and electricity logic optimization - fixed "sprite tigerthresher not found" errors in the Linux version @@ -5262,7 +5477,7 @@ Changes to ragdoll movement/animation logic: - characters are less likely to take impact damage by stumbling in stairs - (+ makes working on the new improved netcode much easier) - ladders can be slid down by holding the sprint key - + Submarine Editor: - zoom now works relative to the mouse's position rather than the center of the screen - fixed selection rectangle not being visible when dragging from bottom right to top left @@ -5274,7 +5489,7 @@ Items: - a "glow effect" when moving items between inventory slots - option to select which location the autopilot should navigate to - fabricator UI shows item descriptions and items that can't be fabricated are grayed out - + Bugfixes: - attempt to fix "DXGI_ERROR_NOT_CURRENTLY_AVAILABLE" errors on startup - fixed water flow sounds taking up all the audio channels and preventing other sounds from playing @@ -5286,13 +5501,13 @@ Bugfixes: - waypoint generation and pathfinding bugfixes Misc: - - improved line of sight effect (instead of a solid black "fog of war", a faint image of the + - improved line of sight effect (instead of a solid black "fog of war", a faint image of the surrounding rooms can be seen through walls) - less ambient light, and it gets darker when diving deeper - - a hull-specific ambient light system: light sources increase the amount of light inside rooms, + - a hull-specific ambient light system: light sources increase the amount of light inside rooms, preventing shadows from looking unnaturally dark in fully lit submarines - option to disable vsync - - added a near-indestructible alien ruin wall variant - breaking through the walls with a railgun + - added a near-indestructible alien ruin wall variant - breaking through the walls with a railgun or a plasma cutter is not always an option anymore - added a parallax effect to the particles floating in the ocean @@ -5314,7 +5529,7 @@ Improved MiniMap (now called "Status Monitor"): - the single player map shows which locations have been visited and the passageways that have been used - minor visual improvements to the single player campaign menus - huskification bugfixes -- oxygen isn't distributed through gaps or vents that are underwater (i.e. air pockets can form when the +- oxygen isn't distributed through gaps or vents that are underwater (i.e. air pockets can form when the sub is flooding) - molochs (or other large creatures) can't push the sub around as easily anymore @@ -5335,7 +5550,7 @@ v0.5.1.2 --------------------------------------------------------------------------------------------------------- - hacked clients can't join a full server or change the name of their character anymore -- option to choose which character to control using the "control" command when there are multiple +- option to choose which character to control using the "control" command when there are multiple characters/creatures with the same name - a console command for spawning items - the server logs show who sent each chat message @@ -5387,7 +5602,7 @@ v0.5.0.3 v0.5.0.2 --------------------------------------------------------------------------------------------------------- -- more server-side sanity checks to prevent (desynced or hacking) players from doing things their +- more server-side sanity checks to prevent (desynced or hacking) players from doing things their characters shouldn't be able to do - fixed collision issues at docking ports (such as shooting up in the air when trying to drop down into a docked shuttle while shuttle hatch is closed) @@ -5431,12 +5646,12 @@ Items: - changes to the logic that determines which item is being highlighted - now it's much easier to select specific items in cramped subs - highlighted items glow (so it's easier to see which item you're targeting in the dark) - - fixed an electricity bug that sometimes caused parts of the grid to not carry any power after + - fixed an electricity bug that sometimes caused parts of the grid to not carry any power after a junction box has been broken and repaired - option to choose the output of a signal check component when the signal doesn't match - fixed fire extinquishers - item search bar in the submarine editor - - fixed cargo items spawning in incorrect positions (which occasionally caused some serious problems + - fixed cargo items spawning in incorrect positions (which occasionally caused some serious problems if the item happened to be a crate full of nitroglycerin) - flares burn longer - fixed flashes from explosions/sparks/flares occasionally ''staying on'' @@ -5590,7 +5805,7 @@ v0.4.0.0 DOCTORS: - medical doctors (can fabricate various drugs/chemicals and give CPR to unconscious characters) - - changes to the dying logic: characters will be unconscious when their health or oxygen goes below 0, + - changes to the dying logic: characters will be unconscious when their health or oxygen goes below 0, and die when it drops to -100 - medical syringes can be used on other characters - any chemicals can be inserted in medical syringes @@ -5601,7 +5816,7 @@ Items: - junction boxes, sonar monitors, navigation terminals and engines break if they're underwater long enough - reactor cools down if it's underwater (multiple fuel rods are required to bring the temperature back up) - forces are applied to items (not just characters) when the submarine hits something - - changes to the logic for distributing oxygen through vents: the oxygen generator pushes more oxygen + - changes to the logic for distributing oxygen through vents: the oxygen generator pushes more oxygen to larger rooms instead of dividing the oxygen output equally between vents - autopilot bugfixes @@ -5617,7 +5832,7 @@ Items: - a bunch of new sprites Multiplayer: - - fixed a bug that caused the server to resend a ton of messages to a client who's been temporarily + - fixed a bug that caused the server to resend a ton of messages to a client who's been temporarily disconnected, causing syncing issues to every player - fixed syncing issues related to items breaking (eg junction boxes being broken only for some players) - fixed dead monsters occasionally ''teleporting'' inside the sub in multiplayer @@ -5673,16 +5888,16 @@ v0.3.5.0 - items float and can be moved around by flowing water - wiring mode which makes wiring more convenient in the editor - networking bugfixes and improvements -- changes to the logic that determines how far the monsters can see/hear the submarine from - now it's +- changes to the logic that determines how far the monsters can see/hear the submarine from - now it's possible to evade some monsters by turning off noisy devices and/or stopping the submarine -- invisible entities (items inside cabinets, hulls/gaps when they've been hidden) can't be highlighted +- invisible entities (items inside cabinets, hulls/gaps when they've been hidden) can't be highlighted or selected in the editor - fixed monster/item spawnpoints being placed in unreachable locations - relay and delay components - fixed lights not being positioned correctly on moving items - added a ''set_color'' connection to light components - ladders outside the sub can be climbed -- changes to drowning/suffocation logic: amount of oxygen drops at a fixed rate instead of effects +- changes to drowning/suffocation logic: amount of oxygen drops at a fixed rate instead of effects "stacking" (e.g. when wearing a diving suit with no oxygen tank in a room with low oxygen) - fixed projectiles not colliding with the submarine when shot from the outside @@ -5698,7 +5913,7 @@ v0.3.4.2 v0.3.4.1 --------------------------------------------------------------------------------------------------------- -- fixed a major bug in the networking code, which caused the server to incorrectly determine the order +- fixed a major bug in the networking code, which caused the server to incorrectly determine the order of messages received from different clients and discard valid messages - fixed levels with the same seed appearing different between the Linux and Windows versions - creatures spawned using the console are synced with clients @@ -5812,8 +6027,8 @@ a round v0.3.2.0 --------------------------------------------------------------------------------------------------------- -- server logs -- server admins have the option to send messages only to dead players and spectators (/d [message]) or +- server logs +- server admins have the option to send messages only to dead players and spectators (/d [message]) or to one specific player (/name [message]) - more reliable door syncing - railgun syncing bugfixes @@ -5860,7 +6075,7 @@ v0.3.1.2 v0.3.1.1 --------------------------------------------------------------------------------------------------------- -- fixed a major bug that caused item/monster ID mismatches between the server and the clients, which +- fixed a major bug that caused item/monster ID mismatches between the server and the clients, which accounted for many of the monster/inventory/item syncing issues - improved player position syncing @@ -5896,7 +6111,7 @@ v0.3.0.4 - fixed "submarine not found" errors which occurred in multiplayer if the filename didn't match the name of the submarine - fixed new structures not lining up with existing ones if switching to editor while a round is running -- fixed a bug in shadow rendering which caused memory leaks +- fixed a bug in shadow rendering which caused memory leaks - the autoupdater only checks the Content folder when deleting files that don't belong to the latest version (i.e. the autoupdater won't delete your mods as long as they aren't saved in the Content folder) - molochs and endworms are immune to bleeding! @@ -5906,9 +6121,9 @@ v0.3.0.3 --------------------------------------------------------------------------------------------------------- - fixed selecting stairs and items outside the sub in editor -- fixed crashing when pressing the ''start'' button while no route is chosen in single player +- fixed crashing when pressing the ''start'' button while no route is chosen in single player - fixed fire syncing -- fixed another bug that crashed the game if in the lobby when a round ends +- fixed another bug that crashed the game if in the lobby when a round ends - camera keeps moving with the sub when typing into chatbox in spectator mode --------------------------------------------------------------------------------------------------------- @@ -5932,7 +6147,7 @@ v0.3.0.1 - fixed a bug that made it impossible to fix broken walls after saving and reloading - fixed crashing when trying to place ladders when no submarine has been loaded - trying to generate waypoints for an empty sub won't crash the game anymore -- when opening the crew commander menu for the first time, there's a text notifying about the hotkey for +- when opening the crew commander menu for the first time, there's a text notifying about the hotkey for opening/closing the menu @@ -5969,7 +6184,7 @@ Items: Submarines: - a new sub, Nehalennia - the collider of the submarine now matches the shape of the hull - - the airlock pumps in each sub are set to pump water out instead of just turning the pump on when pressing + - the airlock pumps in each sub are set to pump water out instead of just turning the pump on when pressing the button outside the airlock Submarine editor: @@ -5977,7 +6192,7 @@ Submarine editor: - tickboxes for hiding hulls, gaps, waypoints and links between items - a list of the most recently used items/structures - placed wires are much easier to move around - - more accurate staircase selecting (the ''bounding box'' of the staircase won't prevent selecting items that + - more accurate staircase selecting (the ''bounding box'' of the staircase won't prevent selecting items that are behind it anymore) - visible indicators for railgun rotation limits @@ -6015,7 +6230,7 @@ Multiplayer: - major changes to the networking code: better lag compensation, more reliable item/character syncing, lower bandwidth consumption - spectator mode - + Submarine: - overloading the electrical grid or the reactor may cause fires @@ -6036,7 +6251,7 @@ Items: Misc: - fixed placing ladders and labels in sub editor - fixed a couple of game-crashing bugs in submarine saving - + --------------------------------------------------------------------------------------------------------- v0.2.5 --------------------------------------------------------------------------------------------------------- @@ -6045,7 +6260,7 @@ Multiplayer: - option to randomly select level seed, submarine and/or game mode - players can be allowed to vote for the next sub and game mode - option to choose character's head - + Submarine: - pressure damage if the submarine dives too deep - added the missing mechanic spawnpoint missing to Aegir @@ -6057,7 +6272,7 @@ Items: - diving suits and mask now obstruct vision when worn - nicer looking sonar monitor -Misc: +Misc: - the levels aren't just enclosed tunnels anymore and it's possible to dive much deeper - settings menu - better UI scaling on small resolutions @@ -6080,7 +6295,7 @@ Multiplayer: - the "fix list" when repairing items is synced between clients, so the reactor can actually be fixed now - more networking optimization - bans can be removed by using a button under the player list, not just by editing the bannedplayers.xml file - + Items: - wires are removed from connection panels when they're deleted in the editor - doors can be rewired from either side @@ -6147,7 +6362,7 @@ v0.2.2 Multiplayer: - network statistics view which can be enabled by opening the debug console (F3) and entering "netstats" (only works if you're running a server) - - updated to latest version of Lidgren networking library, which may or may not have an effect + - updated to latest version of Lidgren networking library, which may or may not have an effect on the chat lag issues Items: @@ -6177,7 +6392,7 @@ Items: - broken doors can only be fixed by mechanics - fixed a bug that sometimes made it impossible to pick/select items after reattaching them on a wall - wires are disconnected and dropped if the item at either end is removed - + --------------------------------------------------------------------------------------------------------- v0.2 --------------------------------------------------------------------------------------------------------- @@ -6226,16 +6441,16 @@ Misc: added in future versions, including one for making custom subs) - an auto-updater in the launcher - the game generates a detailed report if it crashes - - physics optimization (i.e. using simplified physics and animation for off-screen characters and + - physics optimization (i.e. using simplified physics and animation for off-screen characters and disabling them entirely if they're far enough) - - lighting optimization (caching the lights/shadows if a light source hasn't moved instead of + - lighting optimization (caching the lights/shadows if a light source hasn't moved instead of recalculating them every frame) - two new background music tracks - better looking explosions - better looking water particle effects - minor UI improvements - better UI scaling on different resolutions - - health/oxygen bar improvements and status icons for bleeding and water pressure + - health/oxygen bar improvements and status icons for bleeding and water pressure - gap-hull connections are visible in the sub editor - pumps don't have to be manually connected to a hull in the editor anymore, they automatically empty/fill the hull they're inside @@ -6254,7 +6469,7 @@ Multiplayer: to a specific position - a window that displays some network statistics when hosting a server (can be activated by entering "debugview" to the debug console) - + --------------------------------------------------------------------------------------------------------- v0.1.3.1 --------------------------------------------------------------------------------------------------------- @@ -6272,12 +6487,12 @@ Multiplayer: to fly back to it as they try to move away Items: - - putting items inside other items works properly now (i.e. by pulling a spear to the same slot as + - putting items inside other items works properly now (i.e. by pulling a spear to the same slot as a harpoon, not the other way around) - C4 blocks loaded inside a railgun shell won't explode inside the submarine when firing the railgun - fixed another game-crashing railgun bug - fixed a bug that caused characters to spawn with an incorrect number of items - + --------------------------------------------------------------------------------------------------------- v0.1.2 --------------------------------------------------------------------------------------------------------- @@ -6292,7 +6507,7 @@ Items: Other: - optimized lightning and "line of sight" rendering - - an unfinished tutorial which can currently only be accessed by entering "tutorial" into the + - an unfinished tutorial which can currently only be accessed by entering "tutorial" into the debug console --------------------------------------------------------------------------------------------------------- @@ -6301,7 +6516,7 @@ v0.1.1 Multiplayer: - player names are shown - - assigning jobs and selecting job preferences works now (jobs are assigned when the round starts) + - assigning jobs and selecting job preferences works now (jobs are assigned when the round starts) - a menu that shows the crew members and their jobs and skills - reduced lag spikes - fixed a bug that caused disconnected players to stay in the player list diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml index 62d4cca5c..4f6ecf964 100644 --- a/Barotrauma/BarotraumaShared/config.xml +++ b/Barotrauma/BarotraumaShared/config.xml @@ -31,7 +31,7 @@ Down="S" Left="A" Right="D" - Attack="R" + Attack="F" Run="LeftShift" Crouch="LeftControl" InfoTab="Tab" diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 9b96f6b02..34f4c94b2 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -11,7 +11,7 @@ autorestart="false" LevelDifficulty="20" AllowedRandomMissionTypes="Random,Salvage,Monster,Cargo,Combat" - AllowedClientNameChars="32-33,38-46,48-57,65-90,91-91,93-93,95-122,192-255,384-591,1024-1279,19968-40959,13312-19903,131072-15043983,15043985-173791,173824-178207,178208-183983,63744-64255,194560-195103" + AllowedClientNameChars="32-33,38-46,48-57,65-90,91-91,93-93,95-122,192-255,384-591,1024-1279,19968-21327,21329-40959,13312-19903,131072-173791,173824-178207,178208-183983,63744-64255,194560-195103" ServerMessage="" tickrate="20" randomizeseed="True" @@ -24,6 +24,7 @@ startwhenclientsreadyratio="0.8" allowspectating="True" saveserverlogs="True" + linesperlogfile="800" allowragdollbutton="True" allowfiletransfers="True" voicechatenabled="True"