diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index de34b1006..e42aa0547 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()) @@ -348,16 +348,27 @@ namespace Barotrauma 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; + if (limb.ActiveSprite == null) { continue; } + limb.ActiveSprite.Depth = startDepth + depthSortedLimbs.IndexOf(limb) * 0.00001f; } + 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(); } @@ -567,6 +578,11 @@ namespace Barotrauma 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/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 31fc357b6..c5d75f86c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -405,7 +405,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( @@ -1052,8 +1052,18 @@ namespace Barotrauma Position + Vector2.UnitY * 150.0f, Vector2.UnitY * 10.0f, playSound: true, - subId: Submarine?.ID ?? -1);; + subId: Submarine?.ID ?? -1); } } + + partial void OnTalentGiven(string talentIdentifier) + { + GUI.AddMessage(TextManager.Get("talentname." + talentIdentifier.ToString()), + GUI.Style.Yellow, + Position + Vector2.UnitY * 150.0f, + Vector2.UnitY * 10.0f, + playSound: true, + subId: Submarine?.ID ?? -1); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 2027b879d..b85a2b0e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -680,7 +680,7 @@ namespace Barotrauma createAttachmentSlider(info.MoustacheIndex, WearableType.Moustache); createAttachmentSlider(info.FaceAttachmentIndex, WearableType.FaceAttachment); - void createColorSelector(string labelTag, IEnumerable options, Func getter, + void createColorSelector(string labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, Action setter) { var selectorItemRT = createItemRectTransform(labelTag, 0.4f); @@ -714,7 +714,7 @@ namespace Barotrauma dropdown.ListBox.Content.RectTransform), style: "ListBoxElement") { - UserData = option, + UserData = option.Color, CanBeFocused = true }; var colorElement = @@ -723,9 +723,9 @@ namespace Barotrauma scaleBasis: ScaleBasis.Smallest), style: null) { - Color = option, - HoverColor = option, - OutlineColor = Color.Lerp(Color.Black, option, 0.5f), + Color = option.Color, + HoverColor = option.Color, + OutlineColor = Color.Lerp(Color.Black, option.Color, 0.5f), CanBeFocused = false }; } @@ -784,19 +784,9 @@ namespace Barotrauma { OnClicked = (button, o) => { - var headPreset = info.Heads.Keys.GetRandom(Rand.RandSync.Unsynced); - info.Head.gender = headPreset.Gender; - info.Head.race = headPreset.Race; - info.Head.HeadSpriteId = headPreset.ID; - - info.Head.HairIndex = Rand.Int(countAttachmentsOfType(WearableType.Hair), Rand.RandSync.Unsynced); - info.Head.BeardIndex = Rand.Int(countAttachmentsOfType(WearableType.Beard), Rand.RandSync.Unsynced); - info.Head.MoustacheIndex = Rand.Int(countAttachmentsOfType(WearableType.Moustache), Rand.RandSync.Unsynced); - info.Head.FaceAttachmentIndex = Rand.Int(countAttachmentsOfType(WearableType.FaceAttachment), Rand.RandSync.Unsynced); - - info.Head.HairColor = info.HairColors.GetRandom(Rand.RandSync.Unsynced); - info.Head.FacialHairColor = info.FacialHairColors.GetRandom(Rand.RandSync.Unsynced); - info.Head.SkinColor = info.SkinColors.GetRandom(Rand.RandSync.Unsynced); + info.Head = new HeadInfo(); + info.SetGenderAndRace(Rand.RandSync.Unsynced); + info.SetColors(); RecreateFrameContents(); info.RefreshHead(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index e9a30ca3d..945f8acc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -998,10 +998,14 @@ namespace Barotrauma if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { List<(Affliction affliction, string text)> statusIcons = new List<(Affliction affliction, string text)>(); - if (Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 5.0f) + if (Character.InPressure) + { statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); + } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) + { statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + } foreach (Affliction affliction in currentDisplayedAfflictions) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index fb1038f97..88b7bc21d 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. @@ -166,6 +167,8 @@ namespace Barotrauma } } + public float DefaultSpriteDepth { get; private set; } + public WearableSprite HuskSprite { get; private set; } public WearableSprite HerpesSprite { get; private set; } @@ -309,12 +312,23 @@ 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; @@ -383,6 +397,7 @@ namespace Barotrauma return deformations; } } + DefaultSpriteDepth = ActiveSprite.Depth; LightSource?.CheckConditionals(); } @@ -571,8 +586,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) { @@ -593,8 +608,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); } } @@ -618,7 +633,7 @@ namespace Barotrauma } } - if (inWater) + if (InWater) { wetTimer = 1.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index a8f8dd950..e7a0a3af5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -2447,14 +2447,44 @@ 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, LargeFont, 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 += moveDir * 20; + overlapFound = true; + } + tries++; + if (tries > 20) { break; } + } + + messages.Add(newMessage); } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index f1fd031ff..923728eaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -41,13 +41,13 @@ namespace Barotrauma public SpriteSheet SavingIndicator { get; private set; } public UISprite UIGlow { get; private set; } - public UISprite TalentGlow { 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; } @@ -91,8 +91,8 @@ 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.Gray; - public Color ItemQualityColorNormal { get; private set; } = Color.White; + 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; @@ -250,9 +250,6 @@ namespace Barotrauma case "uiglow": UIGlow = new UISprite(subElement); break; - case "talentglow": - TalentGlow = new UISprite(subElement); - break; case "pingcircle": PingCircle = new UISprite(subElement); break; @@ -268,6 +265,9 @@ namespace Barotrauma case "uiglowsolidcircular": UIGlowSolidCircular = new UISprite(subElement); break; + case "uithermalglow": + UIThermalGlow = new UISprite(subElement); + break; case "endroundbuttonpulse": ButtonPulse = new UISprite(subElement); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 7af53c66b..da89b0e4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -141,6 +141,10 @@ namespace Barotrauma { int talentCount = selectedTalents.Count - controlled.Info.UnlockedTalents.Count; talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) + { + talentApplyButton.Flash(GUI.Style.Orange); + } } if (selectedTab != InfoFrameTab.Crew) return; @@ -1184,7 +1188,7 @@ namespace Barotrauma 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, GUIImage glow)> talentButtons = new List<(GUIButton button, GUIComponent icon, GUIImage glow)>(); + 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(); @@ -1453,9 +1457,7 @@ namespace Barotrauma }; } - GUIImage iconGlow = new GUIImage(new RectTransform(Vector2.One, iconImage.RectTransform, anchor: Anchor.Center), sprite: GUI.Style.TalentGlow.Sprite, scaleToFit: true) { Visible = false }; - - talentButtons.Add((talentButton, iconImage, iconGlow)); + talentButtons.Add((talentButton, iconImage)); } talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); @@ -1567,20 +1569,20 @@ namespace Barotrauma string talentIdentifier = talentButton.button.UserData as string; bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); Color newTalentColor = unselectable ? unselectableColor : unselectedColor; - - talentButton.glow.Visible = false; + Color hoverColor = Color.White; if (controlledCharacter.HasTalent(talentIdentifier)) { - newTalentColor = new Color(140,225,140,255); + newTalentColor = GUI.Style.Green; } else if (selectedTalents.Contains(talentIdentifier)) { - newTalentColor = new Color(174,164,124,255); - talentButton.glow.Visible = true; + 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); 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/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/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index da5c38759..e087b99fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -342,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/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 59c300a13..ff08aa3e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -78,7 +78,6 @@ namespace Barotrauma.Items.Components None, HullStatus, ElectricalView, - HullCondition, ItemFinder } @@ -238,7 +237,6 @@ namespace Barotrauma.Items.Components { true when EnableHullStatus => MiniMapMode.HullStatus, true when EnableElectricalView => MiniMapMode.ElectricalView, - true when EnableHullCondition => MiniMapMode.HullCondition, true when EnableItemFinder => MiniMapMode.ItemFinder, _ => MiniMapMode.None }; @@ -263,8 +261,7 @@ namespace Barotrauma.Items.Components 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 = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") }, - new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullCondition") { UserData = MiniMapMode.HullCondition, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.HullCondition.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") } ); @@ -390,9 +387,9 @@ namespace Barotrauma.Items.Components c.Children.ForEach(c2 => c2.CanBeFocused = false); }); - submarineBack.RectTransform.MaxSize = + submarineBack.RectTransform.MaxSize = submarineFront.RectTransform.MaxSize = - submarineContainer.RectTransform.MaxSize = + submarineContainer.RectTransform.MaxSize = new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height); } @@ -493,7 +490,7 @@ namespace Barotrauma.Items.Components displayedSubs.Add(item.Submarine); displayedSubs.AddRange(item.Submarine.DockedTo); - subEntities = MapEntity.mapEntityList.Where(me => me.Submarine == item.Submarine && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList(); + 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; @@ -598,7 +595,6 @@ namespace Barotrauma.Items.Components if (currentMode == MiniMapMode.HullStatus && !EnableHullStatus || currentMode == MiniMapMode.ElectricalView && !EnableElectricalView || - currentMode == MiniMapMode.HullCondition && !EnableHullCondition || currentMode == MiniMapMode.ItemFinder && !EnableItemFinder) { SetDefaultMode(); @@ -606,8 +602,7 @@ namespace Barotrauma.Items.Components modeSwitchButtons[0].Enabled = EnableHullStatus; modeSwitchButtons[1].Enabled = EnableElectricalView; - modeSwitchButtons[2].Enabled = EnableHullCondition; - modeSwitchButtons[3].Enabled = EnableItemFinder; + modeSwitchButtons[2].Enabled = EnableItemFinder; } private void UpdateIDCards(Submarine sub) @@ -642,14 +637,14 @@ namespace Barotrauma.Items.Components return; } - if (currentMode == MiniMapMode.HullStatus || currentMode == MiniMapMode.HullCondition) + if (currentMode == MiniMapMode.HullStatus) { 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.HullCondition && item.Submarine != null) + if (item.Submarine != null) { var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; @@ -849,7 +844,6 @@ namespace Barotrauma.Items.Components switch (currentMode) { case MiniMapMode.HullStatus: - case MiniMapMode.HullCondition: UpdateHullStatus(); miniMapFrame.Visible = true; reportFrame.Visible = true; @@ -992,8 +986,8 @@ namespace Barotrauma.Items.Components { if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; } - isHoveringOver |= - canHoverOverHull && + isHoveringOver |= + canHoverOverHull && (hullStatusComponents[linkedHull].RectComponent == GUI.MouseOn || (draggingReport && hullStatusComponents[linkedHull].RectComponent.MouseRect.Contains(PlayerInput.MousePosition))); if (isHoveringOver) { break; } } @@ -1089,7 +1083,7 @@ namespace Barotrauma.Items.Components } else { - bool hullsVisible = currentMode == MiniMapMode.HullStatus || currentMode == MiniMapMode.HullCondition; + bool hullsVisible = currentMode == MiniMapMode.HullStatus; foreach (var (entity, component) in hullStatusComponents) { @@ -1202,7 +1196,7 @@ namespace Barotrauma.Items.Components 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.HullCondition => 0.1f, MiniMapMode.ElectricalView => 0.1f, _ => 0.5f }; + 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index c708f6538..44d0ad13c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -132,7 +132,15 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; - private static string caveLabel, ruinLabel; + 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index d31ee696e..b2a025ca9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (target == null) { return; } + if (target == null || target.Removed) { return; } Vector2 startPos = GetSourcePos(); startPos.Y = -startPos.Y; @@ -103,7 +103,7 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = FarseerPhysics.ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); barrelPos.Y = -barrelPos.Y; - startPos += barrelPos * item.Scale; + startPos += barrelPos; } } Vector2 endPos = new Vector2(target.DrawPosition.X, target.DrawPosition.Y); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index cb1d306f9..ff50d0fef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System; using System.Linq; using System.Xml.Linq; @@ -58,13 +59,13 @@ namespace Barotrauma.Items.Components }; float x = 1.0f / (1 + RequiredSignalCount); - float y = (x * paddedFrame.Rect.Width) / paddedFrame.Rect.Height; + 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 * y), containerSection.RectTransform, anchor: Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.05f + 0.5f * y) }, + 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++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index f9a7b5e9a..d974c2044 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } [Serialize(false, false)] - public bool SeeThroughWalls + public bool ThermalGoggles { get; private set; @@ -76,6 +76,8 @@ namespace Barotrauma.Items.Components private bool isEquippable; + private float thermalEffectState; + public IEnumerable VisibleCharacters { get @@ -108,7 +110,10 @@ namespace Barotrauma.Items.Components { refEntity = item; } - + + thermalEffectState += deltaTime; + thermalEffectState %= 10000.0f; + if (updateTimer > 0.0f) { updateTimer -= deltaTime; @@ -125,7 +130,7 @@ namespace Barotrauma.Items.Components if (dist < Range * Range) { Vector2 diff = c.WorldPosition - refEntity.WorldPosition; - if (SeeThroughWalls || Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) + if (Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) { visibleCharacters.Add(c); } @@ -180,21 +185,38 @@ namespace Barotrauma.Items.Components } } - if (SeeThroughWalls) + if (ThermalGoggles) { spriteBatch.End(); - GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(Color.Red.ToVector4() * (0.35f + (float)Math.Sin(Timing.TotalTime * 1.6f) * 0.05f)); + 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.03f + (float)Math.Sin(Timing.TotalTime) * 0.01f); + 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); - foreach (Character c in visibleCharacters) + Entity refEntity = equipper; + if (!isEquippable || refEntity == null) { - if (c == character || !c.Enabled || c.Removed) { continue; } + 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) { - limb.Draw(spriteBatch, Screen.Selected.Cam, disableDeformations: true); + 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)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index d2a1e999e..045117832 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -317,7 +317,7 @@ namespace Barotrauma string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; - if (item.Quality > 0) + 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)}"; @@ -1567,7 +1567,7 @@ namespace Barotrauma if (containedItem != null && itemContainer.Inventory.Capacity == 1) { int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); - if (maxStackSize > 1) + if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) { containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 79c6f476d..8e50e4018 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -721,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) @@ -734,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) @@ -1212,8 +1214,8 @@ namespace Barotrauma } if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this && GetComponent() == null) - { - if (Character.Controlled.SelectedConstruction?.GetComponent()?.TargetItem != this && + { + if (Character.Controlled.SelectedConstruction?.GetComponent()?.TargetItem != this && !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) { return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index e4c73b52c..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); @@ -247,7 +249,7 @@ namespace Barotrauma.Lights }*/ spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); - Dictionary visibleHulls = GetVisibleHulls(cam); + Dictionary visibleHulls = GetVisibleHulls(cam); foreach (KeyValuePair hull in visibleHulls) { GUI.DrawRectangle(spriteBatch, @@ -264,12 +266,12 @@ namespace Barotrauma.Lights 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 d86e94f81..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); @@ -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/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index d0547cdbe..3664afb1d 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)) { @@ -457,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 @@ -486,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; } @@ -516,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; } @@ -666,7 +667,7 @@ namespace Barotrauma { if (SelectedList.Contains(entity)) { return; } SelectedList.Add(entity); - HandleDoorGapLinks(entity, + HandleDoorGapLinks(entity, onGapFound: (door, gap) => { door.RefreshLinkedGap(); @@ -674,8 +675,8 @@ namespace Barotrauma { SelectedList.Add(gap); } - }, - onDoorFound: (door, gap) => + }, + onDoorFound: (door, gap) => { if (!SelectedList.Contains(door.Item)) { @@ -719,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; @@ -752,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; @@ -765,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; @@ -816,7 +817,7 @@ namespace Barotrauma posY = -posY; - Vector2[] corners = + Vector2[] corners = { new Vector2(posX, posY), new Vector2(posX + sizeX, posY), @@ -882,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) @@ -907,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); } @@ -947,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)); @@ -1057,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); } @@ -1097,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); @@ -1148,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/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/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index aaad0e557..e72eb2623 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1538,7 +1538,7 @@ namespace Barotrauma 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")); + 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); @@ -3272,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))) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index d37b222fb..74dd10f01 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 068f93785..aec74e517 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index bc48977e1..45c89c098 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index dd27b108a..71e623332 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 6dcdf7a5f..bc313d665 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 9401b8cfb..796c72d08 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1717,16 +1717,84 @@ 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) => + { + Character targetCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false); + 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) => diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fef8b769a..921de6336 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -200,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) @@ -220,12 +220,13 @@ 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 (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)); } @@ -313,7 +314,7 @@ namespace Barotrauma if (success) { - SaveInventories(); + SavePlayers(); yield return CoroutineStatus.Running; 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/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index ad6a7c6e9..81e8e11ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1318,7 +1318,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); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 8fbaabece..41c9a34a9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -374,12 +374,7 @@ namespace Barotrauma.Networking } else { - foreach (Skill skill in characterInfos[i].Job.Skills) - { - var skillPrefab = characterInfos[i].Job.Prefab.Skills.Find(s => skill.Prefab == s); - if (skillPrefab == null) { continue; } - skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.X, SkillReductionOnCampaignMidroundRespawn); - } + ReduceCharacterSkills(characterInfos[i]); } } } @@ -493,6 +488,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/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index dcd61be75..4dc1f8bf0 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.5.0 + 0.1500.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 6e068a3f0..b2bb40191 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -195,6 +195,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index daea35d55..d6e3520d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -105,7 +105,6 @@ namespace Barotrauma protected readonly float colliderWidth; protected readonly float minGapSize; - protected readonly float minHullSize; protected readonly float colliderLength; protected readonly float avoidLookAheadDistance; @@ -119,7 +118,6 @@ namespace Barotrauma colliderLength = size.Y; avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength)); - minHullSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 1.1f); } public virtual void OnAttacked(Character attacker, AttackResult attackResult) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index a59a2e529..c1a35d954 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -148,6 +148,8 @@ namespace Barotrauma private CirclePhase CirclePhase; private float currentAttackIntensity; + private CoroutineHandle disableTailCoroutine; + private readonly IEnumerable myBodies; public LatchOntoAI LatchOntoAI { get; private set; } @@ -556,7 +558,7 @@ namespace Barotrauma } else { - if (Character.Submarine != null) + if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) { @@ -678,8 +680,21 @@ 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) + { + 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; @@ -692,17 +707,43 @@ namespace Barotrauma { SteeringManager.Reset(); Character.AnimController.TargetMovement = Vector2.Zero; - float force = Character.AnimController.SwimSlowParams.SteerTorque; - Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition, force); - if (SelectedAiTarget.Entity is Item item) + if (Character.AnimController.InWater) { - Character.AnimController.Collider.SmoothRotate(MathHelper.ToRadians(item.Rotation), force); + 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); } - Character.AnimController.ApplyPose( - new Vector2(0, -1), - new Vector2(0, -1), - new Vector2(0, -1), - new Vector2(0, -1), footMoveForce: 1); } else { @@ -895,7 +936,7 @@ namespace Barotrauma else if (targetHulls.Any()) { patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); - var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, minGapSize: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n)); + var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, minGapSize: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n)); if (path.Unreachable) { @@ -926,7 +967,7 @@ namespace Barotrauma } if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable) { - PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n)); + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n)); return; } } @@ -938,8 +979,6 @@ namespace Barotrauma UpdateIdle(deltaTime, followLastTarget); } - private bool NodeFilter(PathNode n) => n.Waypoint.CurrentHull == null || n.Waypoint.CurrentHull.Rect.Width > minHullSize && n.Waypoint.CurrentHull.Rect.Height > minHullSize; - private void FindTargetHulls() { if (Character.Submarine == null) { return; } @@ -1479,8 +1518,11 @@ namespace Barotrauma if (!canAttack || distance > margin) { // 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)))) + // 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)); @@ -1489,8 +1531,7 @@ namespace Barotrauma { pathSteering.SteeringSeek(steerPos, weight: 2, minGapWidth: minGapSize, - startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), - nodeFilter: NodeFilter, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true); if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) @@ -1526,7 +1567,7 @@ namespace Barotrauma } else { - pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize, NodeFilter); + pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize); } } else @@ -1750,7 +1791,7 @@ namespace Barotrauma { if (pathSteering != null) { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize, NodeFilter); + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); } else { @@ -1762,7 +1803,7 @@ namespace Barotrauma // Too close UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } - if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } @@ -2282,12 +2323,12 @@ 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); @@ -2299,11 +2340,12 @@ namespace Barotrauma else { // Use path finding - PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize, NodeFilter); + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize); if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable) { // Can't reach State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); return; } } @@ -2419,10 +2461,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; } @@ -2433,6 +2477,7 @@ namespace Barotrauma if (character.CurrentHull != null) { continue; } // Ignore ruins if (hull.Submarine == null) { continue; } + if (hull.Submarine.Info.IsRuin) { continue; } } Door door = null; @@ -2448,14 +2493,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)) @@ -2499,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) @@ -2709,7 +2747,11 @@ namespace Barotrauma { dist *= 0.9f; } - if (!CanPerceive(aiTarget, dist)) { continue; } + + if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget)) + { + continue; + } if (SelectedAiTarget == aiTarget) { @@ -2720,7 +2762,6 @@ namespace Barotrauma //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, addIfNotFound: true); if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) { @@ -2907,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; @@ -3129,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) @@ -3367,6 +3409,12 @@ 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(); AttackingLimb = null; movementMargin = 0; @@ -3382,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 { @@ -3394,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() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 5c2c5d3e8..2378f2e6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -34,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; @@ -1124,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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 90c63c8b8..69270a61e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -26,7 +26,7 @@ namespace Barotrauma public bool AttachToWalls { get; private set; } public bool AttachToCharacters { get; private set; } - private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration; + private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown; private readonly float damageOnDetach, detachStun; private readonly bool weld; private float deattachCheckTimer; @@ -61,6 +61,7 @@ namespace Barotrauma 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)); @@ -283,6 +284,7 @@ namespace Barotrauma if (maxAttachDuration > 0) { deattach = true; + attachCooldown = coolDown; } if (!deattach && targetWall != null && targetSubmarine != null) { @@ -291,7 +293,7 @@ namespace Barotrauma if (enemyAI.CanPassThroughHole(targetWall, targetSection)) { deattach = true; - attachCooldown = 2; + attachCooldown = coolDown; } if (!deattach) { @@ -310,7 +312,7 @@ namespace Barotrauma { deattach = true; character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); - attachCooldown = detachStun * 2; + attachCooldown = Math.Max(detachStun * 2, coolDown); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 0557f1eb3..1f5d54c45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -1048,6 +1048,7 @@ namespace Barotrauma if (closeEnough) { UseWeapon(deltaTime); + character.AIController.SteeringManager.Reset(); } else if (!character.IsFacing(Enemy.WorldPosition)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 03d672ca5..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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 4bad55106..bbaba7ba8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -357,8 +357,11 @@ namespace Barotrauma float itemAngle; Holdable holdable = item.GetComponent(); float torsoRotation = torso.Rotation; - bool equippedInRightHand = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item && rightHand != null && !rightHand.IsSevered; - bool equippedInLefthand = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item && leftHand != null && !leftHand.IsSevered; + + 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); @@ -366,17 +369,23 @@ namespace Barotrauma holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; holdAngle += GetAimWobble(rightHand, leftHand, item); itemAngle = torsoRotation + holdAngle * Dir; + if (holdable.ControlPose) { - var head = GetLimb(LimbType.Head); - if (head != null) + //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) { - 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); + 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; } @@ -506,7 +515,11 @@ namespace Barotrauma 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); + HandIK( + i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], + CurrentAnimationParams.ArmIKStrength, + CurrentAnimationParams.HandIKStrength, + maxAngularVelocity: 15.0f); } } } @@ -531,7 +544,7 @@ namespace Barotrauma return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; } - public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f) + public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f, float maxAngularVelocity = float.PositiveInfinity) { Vector2 shoulderPos; @@ -572,11 +585,20 @@ namespace Barotrauma armAngle -= MathHelper.TwoPi; } - arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false); + 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; - forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false); - float handAngle = forearm != null ? forearmAngle : armAngle; - hand?.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false); + 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) @@ -672,7 +694,7 @@ namespace Barotrauma forearmLength += Vector2.Distance( rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + rightWrist.LimbA.type == LimbType.RightHand ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index c22e642f0..70c639da2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -815,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 0eb6e0eb3..6c964f956 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -170,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; @@ -987,7 +987,7 @@ namespace Barotrauma 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(); @@ -1002,7 +1002,7 @@ 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); - float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()); + float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()), 1.0f); // Limb hand, Vector2 pos, float force = 1.0f HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); } @@ -1012,7 +1012,7 @@ 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); - float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()); + float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()), 1.0f); HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 03f6be102..b04947d5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1212,24 +1212,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) { @@ -1237,7 +1237,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); @@ -1435,7 +1435,7 @@ namespace Barotrauma 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) { @@ -1472,7 +1472,7 @@ namespace Barotrauma Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); foreach (Limb limb in limbs) { - if (!limb.inWater) { continue; } + if (!limb.InWater) { continue; } limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } @@ -1840,9 +1840,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/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 05ac0b9d6..8d50256b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -313,6 +313,15 @@ 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; public List CurrentOrders => Info?.CurrentOrders; @@ -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; @@ -641,22 +658,13 @@ 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 float Bloodloss { @@ -2405,7 +2413,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) && @@ -3813,6 +3821,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; @@ -3950,7 +3959,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; @@ -4369,7 +4381,6 @@ namespace Barotrauma if (!info.UnlockedTalents.Add(talentPrefab.Identifier)) { return false; } } - DebugConsole.AddWarning("added " + talentPrefab.OriginalName); CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); characterTalent.ActivateTalent(addingFirstTime); characterTalents.Add(characterTalent); @@ -4377,7 +4388,10 @@ namespace Barotrauma #if SERVER GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); #endif - + if (addingFirstTime) + { + OnTalentGiven(talentPrefab.Identifier); + } return true; } @@ -4441,6 +4455,7 @@ namespace Barotrauma } 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. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index d6a817716..79d154601 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -451,9 +451,9 @@ namespace Barotrauma set => Head.FaceAttachmentIndex = value; } - public readonly ImmutableArray HairColors; - public readonly ImmutableArray FacialHairColors; - public readonly ImmutableArray SkinColors; + 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 { @@ -522,15 +522,12 @@ namespace Barotrauma // TODO: support for variants Head = new HeadInfo(); HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - Head.gender = GetRandomGender(randSync); HasRaces = CharacterConfigElement.GetAttributeBool("races", false); - Head.race = GetRandomRace(randSync); - CalculateHeadSpriteRange(); - HeadSpriteId = GetRandomHeadID(randSync); + SetGenderAndRace(randSync); Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); - HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray(); + 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)) @@ -580,26 +577,38 @@ namespace Barotrauma 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 = HairColors.GetRandom(); - FacialHairColor = FacialHairColors.GetRandom(); - SkinColor = SkinColors.GetRandom(); + HairColor = SelectRandomColor(HairColors); + FacialHairColor = SelectRandomColor(FacialHairColors); + SkinColor = SelectRandomColor(SkinColors); } private void CheckColors() { if (HairColor == Color.Black) { - HairColor = HairColors.GetRandom(); + HairColor = SelectRandomColor(HairColors); } if (FacialHairColor == Color.Black) { - FacialHairColor = FacialHairColors.GetRandom(); + FacialHairColor = SelectRandomColor(FacialHairColors); } if (SkinColor == Color.Black) { - SkinColor = SkinColors.GetRandom(); + SkinColor = SelectRandomColor(SkinColors); } } @@ -610,7 +619,6 @@ namespace Barotrauma idCounter++; Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); - string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant(); Salary = infoElement.GetAttributeInt("salary", 1000); ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); @@ -642,10 +650,10 @@ namespace Barotrauma { race = GetRandomRace(Rand.RandSync.Unsynced); } - HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray(); - + 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, @@ -655,9 +663,35 @@ namespace Barotrauma infoElement.GetAttributeInt("moustacheindex", -1), infoElement.GetAttributeInt("faceattachmentindex", -1)); - SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); - HairColor = infoElement.GetAttributeColor("haircolor", Color.White); - FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + //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)) @@ -1128,7 +1162,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, Vector2 pos, bool gainedFromApprenticeship = false) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -1148,7 +1182,7 @@ namespace Barotrauma { // 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 AbilityValueStringCharacter(increaseSinceLastSkillPoint, skillIdentifier, Character); + var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromApprenticeship); Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain); foreach (Character character in Character.GetFriendlyCrew(Character)) { @@ -1747,4 +1781,19 @@ namespace Barotrauma 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/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 91fb404b5..6762db6c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -749,7 +749,7 @@ namespace Barotrauma StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; - FaceTint = DefaultFaceTint; + if (Character.GodMode) { return; } for (int i = 0; i < limbHealths.Count; i++) { @@ -775,10 +775,6 @@ namespace Barotrauma { UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); } - Color faceTint = affliction.GetFaceTint(); - if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } - Color bodyTint = affliction.GetBodyTint(); - if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } } @@ -798,10 +794,6 @@ namespace Barotrauma var affliction = afflictions[i]; affliction.Update(this, null, deltaTime); affliction.DamagePerSecondTimer += deltaTime; - Color faceTint = affliction.GetFaceTint(); - if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } - Color bodyTint = affliction.GetBodyTint(); - if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } @@ -818,7 +810,7 @@ namespace Barotrauma } UpdateLimbAfflictionOverlays(); - + UpdateSkinTint(); CalculateVitality(); if (Vitality <= MinVitality) @@ -827,6 +819,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; } @@ -905,6 +923,7 @@ namespace Barotrauma if (Unkillable || Character.GodMode) { return; } var (type, affliction) = GetCauseOfDeath(); + UpdateSkinTint(); Character.Kill(type, affliction); #if CLIENT DisplayVitalityDelay = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index d0df6e71f..4ece5fc65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -218,7 +218,7 @@ namespace Barotrauma public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale; - public bool inWater; + public bool InWater { get; set; } private FixedMouseJoint pullJoint; @@ -535,8 +535,11 @@ 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 { @@ -804,7 +807,7 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime); - if (inWater) + if (InWater) { body.ApplyWaterForces(); } @@ -847,20 +850,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 26fce7320..b469d98d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -14,9 +14,9 @@ namespace Barotrauma NotDefined, Walk, Run, - Crouch, SwimSlow, - SwimFast + SwimFast, + Crouch } abstract class GroundedMovementParams : AnimationParams diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index e6d40d1bc..39016b7d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -79,12 +79,18 @@ namespace Barotrauma [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; } @@ -706,6 +712,9 @@ namespace Barotrauma [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 3b17f0739..fff53e186 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -597,6 +597,9 @@ namespace Barotrauma [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; } @@ -679,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 7d47baf54..49431bb1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -87,19 +87,6 @@ namespace Barotrauma.Abilities public string String { get; set; } } - class AbilityValueStringCharacter : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter - { - public AbilityValueStringCharacter(float value, string abilityString, Character character) - { - Value = value; - String = abilityString; - Character = character; - } - public Character Character { get; set; } - public float Value { get; set; } - public string String { get; set; } - } - class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString { public AbilityStringCharacter(string abilityString, Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index ffde9bcdb..650bf1863 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -125,7 +125,6 @@ namespace Barotrauma.Abilities return null; } - DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier); return characterAbility; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index 3b35a274a..7dde00097 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -1,33 +1,60 @@ 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 impulseStrength; + 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) { - impulseStrength = abilityElement.GetAttributeFloat("impulsestrength", 0f); + 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() { - Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier); - - if (affliction == null) { return; } + 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) { - limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * impulseStrength * (affliction.Strength / affliction.Prefab.MaxStrength), maxVelocity); + 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/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index cabb8a78c..7c7141a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -8,13 +8,13 @@ namespace Barotrauma.Abilities class CharacterAbilityAlienHoarder : CharacterAbility { private readonly float addedDamageMultiplierPerItem; - private readonly int maxAmount; + private readonly float maxAddedDamageMultiplier; private readonly string[] tags; public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f); - maxAmount = abilityElement.GetAttributeInt("maxamount", 0); + maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue); tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); } @@ -30,7 +30,7 @@ namespace Barotrauma.Abilities totalAddedDamageMultiplier += addedDamageMultiplierPerItem; } } - attackData.DamageMultiplier += addedDamageMultiplierPerItem; + attackData.DamageMultiplier += Math.Min(totalAddedDamageMultiplier, maxAddedDamageMultiplier); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index b02fbe021..c6f035c5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityString)?.String is string skillIdentifier && (abilityObject as IAbilityCharacter)?.Character is Character character) + if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character) { - Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f); + Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, Character.Position + Vector2.UnitY * 175.0f, gainedFromApprenticeship: true); } } } 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 ab72e7dc2..a73fb5af0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -845,15 +845,24 @@ namespace Barotrauma var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled; if (character != null) { - character.GiveTalent(args[0]); + 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 itemPrefab in TalentPrefab.TalentPrefabs) + foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs) { - talentNames.Add(itemPrefab.Identifier); + talentNames.Add(talent.DisplayName); } return new string[][] @@ -863,24 +872,15 @@ namespace Barotrauma }; }, isCheat: true)); - commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]]: give the controlled characters all the talents of the specified class", (string[] args) => + commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]] [character]: give the specified character all the talents of the specified class", (string[] args) => { - if (Character.Controlled == null) { return; } - if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) - { - foreach (var talentTree in TalentTree.JobTalentTrees) - { - foreach (var subTree in talentTree.Value.TalentSubTrees) - { - foreach (var option in subTree.TalentOptionStages) - { - foreach (var talent in option.Talents) - { - Character.Controlled.GiveTalent(talent); - } - } - } - } + 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 { @@ -893,15 +893,21 @@ namespace Barotrauma if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) { ThrowError($"No talents configured for the job \"{args[0]}\"."); - return; + 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.Controlled.GiveTalent(talent); + character.GiveTalent(talent); + NewMessage($"Unlocked talent \"{talent.DisplayName}\"."); } } } @@ -913,7 +919,8 @@ namespace Barotrauma availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name)); return new string[][] { - availableArgs.ToArray() + availableArgs.ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() }; }, isCheat: true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 17ab504c6..8959f518f 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; 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/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/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index bfa584f9c..2c8f53636 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -359,10 +359,19 @@ namespace Barotrauma 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((int)(baseExperienceGain * experienceGainMultiplier.Value), isMissionExperience: true); + 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 9b52a0651..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,18 +288,19 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, 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 ?? chosenPosition.Ruin?.Submarine)); - spawnPos = spawnPoint.WorldPosition; + 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) @@ -447,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/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index dbff41f5b..aa8c2eee6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -10,10 +10,15 @@ namespace Barotrauma { 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; } @@ -23,6 +28,7 @@ namespace Barotrauma subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); } + float difficultyModifier = GetLevelDifficultyModifier(); foreach (var sub in Submarine.Loaded) { if (sub.Info.Type == SubmarineType.Player || @@ -32,7 +38,7 @@ namespace Barotrauma { continue; } - Place(sub.ToEnumerable()); + Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier); } if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost) @@ -42,12 +48,23 @@ namespace Barotrauma } } - public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer) + 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() { - Place(sub.ToEnumerable(), regeneratedContainer); + return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier); } - private static void Place(IEnumerable subs, ItemContainer regeneratedContainer = null) + 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) { @@ -167,7 +184,7 @@ namespace Barotrauma } foreach (var validContainer in validContainers) { - var newItems = SpawnItem(itemPrefab, containers, validContainer); + var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier); if (newItems.Any()) { spawnedItems.AddRange(newItems); @@ -201,11 +218,18 @@ namespace Barotrauma return validContainers; } - private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) + private static readonly float[] qualityCommonnesses = new float[Quality.MaxQuality + 1] + { + 0.85f, + 0.125f, + 0.0225f, + 0.0025f, + }; + + private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) { List spawnedItems = new List(); - bool success = false; - if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return spawnedItems; } + 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")) { @@ -220,10 +244,24 @@ namespace Barotrauma break; } if (!validContainer.Key.Inventory.CanBePut(itemPrefab)) { break; } + + int quality = 0; + float qualityCommmonnessSum = qualityCommonnesses.Sum(); + float randomNumber = Rand.Range(0f, qualityCommmonnessSum, Rand.RandSync.Server); + for (int k = qualityCommonnesses.Length - 1; k >= 0; k--) + { + if (randomNumber < qualityCommonnesses[k]) + { + quality = k; + 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) @@ -235,7 +273,6 @@ namespace Barotrauma spawnedItems.Add(item); validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false); containers.AddRange(item.GetComponents()); - success = true; } return spawnedItems; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 5d89b421e..e526eebd8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -563,13 +563,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs new file mode 100644 index 000000000..f73cebac4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -0,0 +1,280 @@ +#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")] + 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 = 0), 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; } + + SpawnTimerGoal ??= Rand.Range(Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y), Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y), 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"; + + switch (connection.Name) + { + case "set_state": + CanSpawn = isNonZero; + break; + case "toggle" when isNonZero: + CanSpawn = !CanSpawn; + 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; + } + } + + 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/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index d14b487f5..b2c819360 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -598,9 +598,12 @@ namespace Barotrauma.Items.Components 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 == item.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); + i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index cce65f786..05b87be87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -77,6 +77,7 @@ namespace Barotrauma.Items.Components }; } item.IsShootable = true; + item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); } @@ -210,7 +211,7 @@ namespace Barotrauma.Items.Components 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)); + 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 @@ -222,16 +223,16 @@ namespace Barotrauma.Items.Components else { // TODO: We might want to make this configurable - hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f); + hitPos -= deltaTime * 15f; if (Swing) { - ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos); + 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.PiOver2) + if (hitPos < -MathHelper.Pi) { RestoreCollision(); hitting = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index ee11e6001..0966ea92f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -74,8 +74,8 @@ namespace Barotrauma.Items.Components { 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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 4aaceab07..bc9c658ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -712,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/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index f97b2f01f..dc1ea9383 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(item.Rotation) > 0.01f) { Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); - transformedItemPos = Vector2.Transform(transformedItemPos, transform); + transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position; transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); @@ -550,6 +550,10 @@ namespace Barotrauma.Items.Components currentRotation *= item.body.Dir; currentRotation += item.body.Rotation; } + else + { + currentRotation += MathHelper.ToRadians(-item.Rotation); + } int i = 0; Vector2 currentItemPos = transformedItemPos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index 38d3c40fa..39049b9bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -64,13 +64,6 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(true, true, description: "Enable hull condition mode.")] - public bool EnableHullCondition - { - get; - set; - } - [Editable, Serialize(true, true, description: "Enable item finder mode.")] public bool EnableItemFinder { @@ -148,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.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f); + waterAmount = Rand.Range(0.0f, 1.0f); } else { - hullData.ReceivedWaterAmount = 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.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/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index aaf95429a..5ffe84c32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -9,6 +9,14 @@ namespace Barotrauma.Items.Components { public const int MaxQuality = 3; + public static readonly float[] QualityCommonnesses = new float[] + { + 0.8f, + 0.15f, + 0.045f, + 0.005f, + }; + public enum StatType { Condition, @@ -39,7 +47,18 @@ namespace Barotrauma.Items.Components public int QualityLevel { get { return qualityLevel; } - set { qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); } + 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index b0885d400..041037cdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -376,7 +376,7 @@ namespace Barotrauma.Items.Components tinkeringDuration -= deltaTime; // not great to interject it here, should be less reliant on returning - float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.MaxCondition) * 100f; + float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.Prefab.Health) * 100f; item.Condition -= conditionDecrease; if (!CanTinker(CurrentFixer) || tinkeringDuration <= 0f) @@ -424,7 +424,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); @@ -458,7 +459,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index f6b290bc3..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 } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 200414d89..70d4403b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -178,7 +178,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/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index ccb06ad4a..e9f28b882 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 2d59645bb..2d5079340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -106,7 +106,7 @@ namespace Barotrauma //a dictionary containing lists of the status effects in all the components of the item private readonly bool[] hasStatusEffectsOfType; private readonly Dictionary> statusEffectLists; - + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -232,7 +232,7 @@ namespace Barotrauma public bool IsInteractable(Character character) { if (character != null && character.IsOnPlayerTeam) - { + { return IsPlayerTeamInteractable; } else @@ -254,6 +254,12 @@ namespace Barotrauma { if (!Prefab.AllowRotatingInEditor) { return; } rotationRad = MathHelper.ToRadians(value); +#if CLIENT + if (Screen.Selected == GameMain.SubEditorScreen) + { + SetContainedItemPositions(); + } +#endif } } @@ -478,7 +484,7 @@ namespace Barotrauma 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, //we'll just use NaN (which does nothing) and set the default value in the constructor/load [Serialize(float.NaN, false), Editable] @@ -628,9 +634,9 @@ namespace Barotrauma public int Quality { - get - { - return qualityComponent?.QualityLevel ?? 0; + get + { + return qualityComponent?.QualityLevel ?? 0; } set { @@ -1155,7 +1161,7 @@ namespace Barotrauma { return GetComponent()?.GetValue(statType) ?? 0.0f; } - + public void RemoveContained(Item contained) { ownInventory?.RemoveItem(contained); @@ -1867,7 +1873,8 @@ namespace Barotrauma foreach (ItemComponent component in components) { component.FlipX(relativeToSub); - } + } + SetContainedItemPositions(); } public override void FlipY(bool relativeToSub) @@ -1892,6 +1899,7 @@ namespace Barotrauma { component.FlipY(relativeToSub); } + SetContainedItemPositions(); } /// @@ -2112,8 +2120,6 @@ namespace Barotrauma } while (CoroutineManager.DeltaTime <= 0.0f); delayedSignals.Remove((signal, connection)); - - signal.source = this; connection.SendSignal(signal); yield return CoroutineStatus.Success; @@ -2470,6 +2476,8 @@ namespace Barotrauma parentInventory.RemoveItem(this); parentInventory = null; } + + SetContainedItemPositions(); } public void Equip(Character character) @@ -2741,7 +2749,7 @@ namespace Barotrauma { CoroutineManager.StopCoroutines(logPropertyChangeCoroutine); } - logPropertyChangeCoroutine = CoroutineManager.InvokeAfter(() => + logPropertyChangeCoroutine = CoroutineManager.Invoke(() => { GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); }, delay: 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index ded5ee66a..3378966a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -920,7 +920,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 + "\"!"); } @@ -1152,11 +1153,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 350fb0795..ac3521e32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1829,7 +1829,7 @@ namespace Barotrauma private void GenerateRuin(Point ruinPos, bool mirror) { - var ruinGenerationParams = RuinGenerationParams.GetRandom(); + var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server); LocationType locationType = StartLocation?.Type; if (locationType == null) @@ -1839,7 +1839,7 @@ namespace Barotrauma { locationType = LocationType.List.Where(lt => ruinGenerationParams.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); } } @@ -3594,7 +3594,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); } } @@ -3953,7 +3953,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index be1f4811e..be359698a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -44,7 +44,7 @@ namespace Barotrauma.RuinGeneration this.filePath = filePath; } - public static RuinGenerationParams GetRandom() + public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server) { if (paramsList == null) { LoadAll(); } @@ -54,7 +54,7 @@ namespace Barotrauma.RuinGeneration 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() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 37c1c3ab8..6c208d35b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -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; /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 76b284bdc..aac3655eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -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.1f; + const float BaseDifficulty = -3f; + return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty; + } } partial void GenerateLocationConnectionVisuals(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 70e682b0b..224b41e02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -85,7 +85,15 @@ namespace Barotrauma var subInfo = new SubmarineInfo(outpostModuleFile.Path); if (subInfo.OutpostModuleInfo != null) { - if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") != generationParams is RuinGeneration.RuinGenerationParams) { continue; } + 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; + } + } outpostModules.Add(subInfo); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index d142fadbc..e53436daf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -11,6 +11,7 @@ using System.Xml.Linq; using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; +using Barotrauma.Lights; #endif namespace Barotrauma @@ -97,7 +98,7 @@ namespace Barotrauma { get { return Prefab.Body; } } - + public List Bodies { get; private set; } public bool CastShadow @@ -113,7 +114,7 @@ namespace Barotrauma } private float? maxHealth; - + [Serialize(100.0f, true)] public float MaxHealth { @@ -168,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 @@ -176,7 +177,7 @@ namespace Barotrauma get { return spriteColor; } set { spriteColor = value; } } - + [Editable, Serialize(false, true)] public bool UseDropShadow { @@ -216,6 +217,13 @@ namespace Barotrauma UpdateSections(); } } + +#if CLIENT + foreach (LightSource light in Lights) + { + light.SpriteScale = scale * textureScale; + } +#endif } } @@ -231,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 } } @@ -239,7 +254,13 @@ namespace Barotrauma public Vector2 TextureOffset { get { return textureOffset; } - set { textureOffset = value; } + set + { + textureOffset = value; +#if CLIENT + SetLightTextureOffset(); +#endif + } } @@ -282,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; } @@ -364,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 } @@ -375,7 +402,7 @@ namespace Barotrauma defaultRect = rectangle; maxHealth = sp.Health; - + rect = rectangle; TextureScale = sp.TextureScale; @@ -427,13 +454,48 @@ 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) @@ -445,7 +507,7 @@ namespace Barotrauma } InsertToList(); - + DebugConsole.Log("Created " + Name + " (" + ID + ")"); } @@ -477,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)); @@ -505,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); @@ -549,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) @@ -646,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 @@ -693,6 +755,10 @@ namespace Barotrauma #if CLIENT if (convexHulls != null) convexHulls.ForEach(x => x.Remove()); + foreach (LightSource light in Lights) + { + light.Remove(); + } #endif } @@ -725,6 +791,10 @@ namespace Barotrauma #if CLIENT if (convexHulls != null) convexHulls.ForEach(x => x.Remove()); + foreach (LightSource light in Lights) + { + light.Remove(); + } #endif } @@ -782,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; } @@ -800,7 +870,7 @@ namespace Barotrauma Upgrades.Add(upgrade); upgrade.ApplyUpgrade(); } - + UpdateSections(); return true; @@ -915,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; @@ -926,7 +996,7 @@ namespace Barotrauma } return sectionPos; } - } + } public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) { @@ -949,7 +1019,7 @@ namespace Barotrauma GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); #endif } - } + } #if CLIENT if (playSound && damageAmount > 0) { @@ -976,7 +1046,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) { @@ -1022,10 +1092,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 @@ -1034,7 +1104,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)); } } @@ -1053,7 +1123,7 @@ namespace Barotrauma gapRect.Y += 10; gapRect.Width += 20; gapRect.Height += 20; - + bool horizontalGap = !IsHorizontal; if (Prefab.BodyRotation != 0.0f) { @@ -1090,7 +1160,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; @@ -1106,9 +1176,9 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical", + attacker.Info?.IncreaseSkillLevel("mechanical", -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f), - SectionPosition(sectionIndex)); + SectionPosition(sectionIndex)); } } } @@ -1116,7 +1186,7 @@ namespace Barotrauma bool hasHole = SectionBodyDisabled(sectionIndex); if (hadHole == hasHole) { return; } - + UpdateSections(); } @@ -1198,7 +1268,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; } @@ -1240,7 +1310,7 @@ namespace Barotrauma } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation); - + public override void FlipX(bool relativeToSub) { base.FlipX(relativeToSub); @@ -1261,7 +1331,7 @@ namespace Barotrauma CreateStairBodies(); } - + if (HasBody) { CreateSections(); @@ -1388,7 +1458,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 @@ -1433,7 +1503,7 @@ namespace Barotrauma } SerializableProperty.SerializeProperties(this, element); - + foreach (var upgrade in Upgrades) { upgrade.Save(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 380df3975..19189ff95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -291,7 +291,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 3e92a9aea..3273bb4c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -914,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) @@ -1429,6 +1431,11 @@ namespace Barotrauma } } } + else if (info.IsRuin) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + } } if (entityGrid != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 3325ea591..e335f0eec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -868,11 +868,12 @@ namespace Barotrauma } } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = 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.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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index cb7f26141..1f4ff6489 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -92,7 +92,7 @@ namespace Barotrauma } 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 780aaa767..262bbb9c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -15,6 +16,22 @@ namespace Barotrauma { public static class XMLExtensions { + 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) => System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri); public static readonly XmlReaderSettings ReaderSettings = new XmlReaderSettings @@ -485,6 +502,25 @@ namespace Barotrauma 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(); @@ -525,12 +561,28 @@ namespace Barotrauma => $"{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:X2}"; + => $"#{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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index d0f70222e..2c8cf176c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -197,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})"; @@ -274,6 +286,8 @@ namespace Barotrauma private readonly bool spawnItemRandomly; private readonly List spawnCharacters; + public readonly List giveTalentInfos; + private readonly List aiTriggers; private readonly List triggeredEvents; @@ -374,6 +388,7 @@ namespace Barotrauma spawnItems = new List(); spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); spawnCharacters = new List(); + giveTalentInfos = new List(); aiTriggers = new List(); Afflictions = new List(); Explosions = new List(); @@ -576,7 +591,7 @@ namespace Barotrauma break; case "requiredaffliction": requiredAfflictions ??= new HashSet<(string, float)>(); - string[] ids = subElement.GetAttributeStringArray("identifier", new string[0]); + string[] ids = subElement.GetAttributeStringArray("identifier", null) ?? subElement.GetAttributeStringArray("type", new string[0]); foreach (string afflictionId in ids) { requiredAfflictions.Add(( @@ -669,6 +684,10 @@ 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; @@ -1305,6 +1324,33 @@ namespace Barotrauma } } } + + if (giveTalentInfos.Any() && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)) + { + 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) @@ -1367,6 +1413,7 @@ namespace Barotrauma }); } } + if (spawnItemRandomly) { SpawnItem(spawnItems.GetRandom()); 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 88835bfc0..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()) diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 56e627677..94ea2fd37 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,54 @@ +--------------------------------------------------------------------------------------------------------- +v0.1500.6.0 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Improvements and additions to the alien ruins and the new fractal guardians. +- More improvements and fixes to the overhauled character sprites and animations. +- 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. +- Talent adjustments and balancing. +- Nerfed thermal goggles: some enemies are invisible to them and targets behind walls are much less clear. +- 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. +- Made welding tools a bit less effective early to compensate for increases to their effectiveness from quality/talents. +- Fuel tanks, rods, grenades and other quality-based items can be stacked again. Items of different qualities still can't be put in the same stack. +- Endocrine Booster now gives a random talent. +- New sprite for the mudraptor beak grown by splicing mudraptor genes. +- Fabricating fuel rods now requirse 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). +- Changed how skillbooks work: the skills are gained when you "finish" reading the book instead of continuously to prevent UI message spam and overlap, the books can be used on others in the health interface or by hitting them with the book. +- Color the selected (but unapplied) talent icons orange instead of changing the icon in the talent UI, made the apply button flash when there's unapplied talents. +- Merged status monitor's status and hull condition tabs. +- Halved concussion's damage threshold to make it possible for more sources of damage to trigger it, + halved the probability to compensate. + +Fixes: +- Fixed high-quality batteries/tanks (or other items with an increased max condition) spawning in non-full condition (unstable only). +- Fixed ropes sometimes crashing the game (unstable only). +- Fixed some items occasionally disappearing from outposts when re-entering them. The most noticeable symptom was wires disappearing from the outpost's airlock, preventing the hatch from opening (unstable only). +- Fixed multiplayer campaign characters resetting if they're dead when the round ends. +- Fixed clients who aren't currently controlling a character not getting XP in the mp campaign (unstable only). +- 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 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. +- Fixed boarding axe (unstable only). +- 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 sub editor where changing ItemComponent color with HSV picker would create an error in the console. +- Fixed inability to hit downed characters with short melee weapons like the diving knife. +- Fixed inability to sell nasonov and faraday artifacts (unstable only). +- Fixed concussion description (unstable only). +- Fixes gender-specific affliction sound effects (e.g. vomiting) not playing (unstable only). +- Fixed humans' "aim source position" being too low, causing aim to be slightly off (unstable only). +- Fixed artifact transport case displaying as empty when there's a nasonov/faraday artifact inside (unstable only). +- Fixed "infiltration" event getting stuck on one of the conversation options. +- Fixed ruin's physics bodies being dynamic in mirrored levels (causing them to sink). +- Backwards compatibility: assign skin colors to characters saved in previous versions. +- Assign random skin/hair colors to characters without one configured instead of defaulting to white. Fixes all tutorial characters having white skin/hair. +Modding: +- 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. + --------------------------------------------------------------------------------------------------------- v0.1500.5.0 --------------------------------------------------------------------------------------------------------- @@ -8,7 +59,6 @@ Additions and changes: - New Alien Ruin mission: kill the guardians inhabiting the ruin and destroy their pods. - Improvements and fixes to the overhauled character sprites and animations. - More talent improvements and additions. -- Balanced loot spawn rates. - Cap the amount argument of the spawnitem command to 100 to prevent freezing/crashing when trying to spawn a ridiculous amount of the item. - Nerfed concussions: they now require a larger amount of damage to the head to trigger and slowly heal by themselves. - Added "unlocktalents [job]" command. @@ -19,6 +69,17 @@ Additions and changes: - Added button to randomize character appearance in the character customization menus. - Permanently reduce character skills 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. +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. +- Modifed characters' base vitalities. +- Adjustments to monster stats. +- Reduced mission experience gains, level difficulty affects mission experience. + Fixes: - Fixed crash when firing a syring gun (unstable only). - Fixed crashing when using meds in multiplayer with friendly fire turned off (unstable only).