diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 839d52d50..187b87f7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -36,8 +36,8 @@ namespace Barotrauma private float minZoom = 0.1f; public float MinZoom { - get { return minZoom;} - set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } + get { return minZoom; } + set { minZoom = MathHelper.Clamp(value, 0.001f, 10.0f); } } private float maxZoom = 2.0f; @@ -63,7 +63,7 @@ namespace Barotrauma private float prevZoom; public float Shake; - private Vector2 shakePosition; + public Vector2 ShakePosition { get; private set; } private float shakeTimer; private float globalZoomScale = 1.0f; @@ -371,7 +371,7 @@ namespace Barotrauma if (Shake < 0.01f) { - shakePosition = Vector2.Zero; + ShakePosition = Vector2.Zero; shakeTimer = 0.0f; } else @@ -379,11 +379,11 @@ namespace Barotrauma shakeTimer += deltaTime * 5.0f; Vector2 noisePos = new Vector2((float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(shakeTimer, shakeTimer, 0.5f) - 0.5f); - shakePosition = noisePos * Shake * 2.0f; + ShakePosition = noisePos * Shake * 2.0f; Shake = MathHelper.Lerp(Shake, 0.0f, deltaTime * 2.0f); } - Translate(moveCam + shakePosition); + Translate(moveCam + ShakePosition); Freeze = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs index b1392f9cb..7adc4fb99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs @@ -6,6 +6,8 @@ namespace Barotrauma { class CameraTransition { + private static List activeTransitions = new List(); + public bool Running { get; @@ -19,6 +21,7 @@ namespace Barotrauma private readonly float? endZoom; public readonly float WaitDuration; + public float EndWaitDuration = 0.1f; public readonly float PanDuration; public readonly bool FadeOut; public readonly bool LosFadeIn; @@ -29,6 +32,8 @@ namespace Barotrauma public bool AllowInterrupt = false; public bool RemoveControlFromCharacter = true; + + public bool RunWhilePaused = true; public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null) { @@ -45,8 +50,19 @@ namespace Barotrauma if (targetEntity == null) { return; } Running = true; - CoroutineManager.StopCoroutines("CameraTransition"); + + prevControlled = Character.Controlled; + activeTransitions.RemoveAll(a => !CoroutineManager.IsCoroutineRunning(a.updateCoroutine)); + foreach (var activeTransition in activeTransitions) + { + if (activeTransition.prevControlled != null) + { + prevControlled ??= activeTransition.prevControlled; + } + activeTransition.Stop(); + } updateCoroutine = CoroutineManager.StartCoroutine(Update(targetEntity, cam), "CameraTransition"); + activeTransitions.Add(this); } public void Stop() @@ -62,11 +78,13 @@ namespace Barotrauma #endif } + private float DeltaTime => CoroutineManager.Paused && !RunWhilePaused ? 0 : CoroutineManager.DeltaTime; + private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } - prevControlled = Character.Controlled; + prevControlled ??= Character.Controlled; if (RemoveControlFromCharacter) { #if CLIENT @@ -80,6 +98,7 @@ namespace Barotrauma float endZoom = this.endZoom ?? 0.5f; Vector2 initialCameraPos = cam.Position; Vector2? initialTargetPos = targetEntity?.WorldPosition; + Vector2 endPos = cam.Position; float timer = -WaitDuration; @@ -137,13 +156,13 @@ namespace Barotrauma { startPos += targetEntity.WorldPosition - initialTargetPos.Value; } - Vector2 endPos = cameraEndPos.HasValue ? + endPos = cameraEndPos.HasValue ? new Vector2( MathHelper.Lerp(minPos.X, maxPos.X, (cameraEndPos.Value.ToVector2().X + 1.0f) / 2.0f), MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) : prevControlled?.WorldPosition ?? targetEntity.WorldPosition; - Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration); + Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration) + cam.ShakePosition; cam.Translate(cameraPos - cam.Position); #if CLIENT @@ -162,14 +181,21 @@ namespace Barotrauma Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity); } #endif - timer += CoroutineManager.DeltaTime; + timer += DeltaTime; yield return CoroutineStatus.Running; } - Running = false; + float endTimer = 0.0f; + while (endTimer <= EndWaitDuration) + { + cam.Translate(endPos - cam.Position); + cam.Zoom = endZoom; + endTimer += DeltaTime; + yield return CoroutineStatus.Running; + } - yield return new WaitForSeconds(0.1f); + Running = false; #if CLIENT GUI.ScreenOverlayColor = Color.TransparentBlack; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index a9fa83ac4..6f3eda991 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -99,12 +99,14 @@ namespace Barotrauma float newAngularVelocity = Collider.AngularVelocity; Collider.CorrectPosition(character.MemState, out newPosition, out newVelocity, out newRotation, out newAngularVelocity); - newVelocity = newVelocity.ClampLength(100.0f); - if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } - overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; - - Collider.LinearVelocity = newVelocity; - Collider.AngularVelocity = newAngularVelocity; + if (Collider.BodyType == BodyType.Dynamic) + { + newVelocity = newVelocity.ClampLength(100.0f); + if (!MathUtils.IsValid(newVelocity)) { newVelocity = Vector2.Zero; } + overrideTargetMovement = newVelocity.LengthSquared() > 0.01f ? newVelocity : Vector2.Zero; + Collider.LinearVelocity = newVelocity; + Collider.AngularVelocity = newAngularVelocity; + } float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); float errorTolerance = character.CanMove && !character.IsRagdolled ? 0.01f : 0.2f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index d20050fe4..0f86bcc9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,26 @@ namespace Barotrauma set => grainStrength = Math.Max(0, value); } + /// + /// Can be used by status effects + /// + public float CollapseEffectStrength + { + get { return Level.Loaded?.Renderer?.CollapseEffectStrength ?? 0.0f; } + set + { + if (Level.Loaded?.Renderer == null) { return; } + if (Controlled == this) + { + float strength = MathHelper.Clamp(value, 0.0f, 1.0f); + Level.Loaded.Renderer.CollapseEffectStrength = strength; + Level.Loaded.Renderer.CollapseEffectOrigin = Submarine?.WorldPosition ?? WorldPosition; + Screen.Selected.Cam.Shake = Math.Max(MathF.Pow(strength, 3) * 100.0f, Screen.Selected.Cam.Shake); + Screen.Selected.Cam.Rotation = strength * (PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, (float)Timing.TotalTime * 0.05f) - 0.5f); + Level.Loaded.Renderer.ChromaticAberrationStrength = value * 50.0f; + } + } + } /// /// Can be used to set camera shake from status effects /// @@ -278,6 +298,21 @@ namespace Barotrauma { keys[i].SetState(); } + + if (CharacterInventory.IsMouseOnInventory && CharacterHUD.ShouldDrawInventory(this)) + { + ResetInputIfPrimaryMouse(InputType.Use); + ResetInputIfPrimaryMouse(InputType.Shoot); + ResetInputIfPrimaryMouse(InputType.Select); + void ResetInputIfPrimaryMouse(InputType inputType) + { + if (GameSettings.CurrentConfig.KeyMap.Bindings[inputType].MouseButton == MouseButton.PrimaryMouse) + { + keys[(int)inputType].Reset(); + } + } + } + //if we were firing (= pressing the aim and shoot keys at the same time) //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) @@ -296,8 +331,7 @@ namespace Barotrauma float targetOffsetAmount = 0.0f; if (moveCam) { - if (NeedsAir && !IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure > 0.0f)) { float pressure = AnimController.CurrentHull == null ? 100.0f : AnimController.CurrentHull.LethalPressure; if (pressure > 0.0f) @@ -911,7 +945,7 @@ namespace Barotrauma { name += " " + TextManager.Get("Disguised"); } - else if (Info.Title != null) + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) { name += '\n' + Info.Title; } @@ -987,13 +1021,13 @@ namespace Barotrauma } } - if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) + if (Params.ShowHealthBar && CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) { hudInfoAlpha = Math.Max(hudInfoAlpha, Math.Min(CharacterHealth.DamageOverlayTimer, 1.0f)); Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), - CharacterHealth.DisplayedVitality / MaxVitality, + CharacterHealth.DisplayedVitality / MaxVitality, Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 80b153e28..090a8e40d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -13,9 +13,8 @@ namespace Barotrauma { const float BossHealthBarDuration = 120.0f; - class BossHealthBar + abstract class BossProgressBar { - public readonly Character Character; public float FadeTimer; public readonly GUIComponent TopContainer; @@ -24,9 +23,18 @@ namespace Barotrauma public readonly GUIProgressBar TopHealthBar; public readonly GUIProgressBar SideHealthBar; - public BossHealthBar(Character character) + public abstract bool Completed { get; } + + public abstract bool Interrupted { get; } + + public abstract float State { get; } + + public abstract string NumberToDisplay { get; } + + public abstract Color Color { get; } + + public BossProgressBar(LocalizedString label) { - Character = character; FadeTimer = BossHealthBarDuration; TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) @@ -34,7 +42,7 @@ namespace Barotrauma MinSize = new Point(100, 50), RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), label, textAlignment: Alignment.Center, textColor: GUIStyle.Red); TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) @@ -42,22 +50,95 @@ namespace Barotrauma { Color = GUIStyle.Red }; + CreateNumberText(TopHealthBar); SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), label, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { Color = GUIStyle.Red }; + CreateNumberText(SideHealthBar); TopContainer.Visible = SideContainer.Visible = false; TopContainer.CanBeFocused = false; TopContainer.Children.ForEach(c => c.CanBeFocused = false); SideContainer.CanBeFocused = false; SideContainer.Children.ForEach(c => c.CanBeFocused = false); + + void CreateNumberText(GUIComponent parent) + { + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform) + { AbsoluteOffset = new Point(2) }, + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDark) + { + TextGetter = () => NumberToDisplay + }; + new GUITextBlock(new RectTransform(Vector2.One, parent.RectTransform), + string.Empty, textAlignment: Alignment.Center, textColor: GUIStyle.TextColorBright) + { + TextGetter = () => NumberToDisplay + }; + } + } + + public abstract bool IsDuplicate(object targetObject); + } + + class BossHealthBar : BossProgressBar + { + public readonly Character Character; + + public override float State => Character.Vitality / Character.MaxVitality; + + public override bool Completed => Character.IsDead; + + public override bool Interrupted => Character.Removed || !Character.Enabled; + + public override Color Color => + Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.PoisonType) > 0 || Character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.ParalysisType) > 0 ? + GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; + + public override string NumberToDisplay => string.Empty; + + public BossHealthBar(Character character) : base(character.DisplayName) + { + Character = character; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Character character && Character == character; + } + } + + class MissionProgressBar : BossProgressBar + { + public readonly Mission Mission; + + public override float State => Mission.State / (float)Mission.Prefab.MaxProgressState; + + public override bool Completed => Mission.State >= Mission.Prefab.MaxProgressState; + + public override bool Interrupted => Mission.Failed || GameMain.GameSession?.Missions == null || !GameMain.GameSession.Missions.Contains(Mission); + + public override Color Color => GUIStyle.Red; + + public override string NumberToDisplay => Mission.Prefab.ShowProgressInNumbers ? + $"{Mission.State}/{Mission.Prefab.MaxProgressState}" : + string.Empty; + + public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) + { + Mission = mission; + } + + public override bool IsDuplicate(object targetObject) + { + return targetObject is Mission mission && Mission == mission; } } @@ -69,7 +150,7 @@ namespace Barotrauma private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; - private static readonly List bossHealthBars = new List(); + private static readonly List bossProgressBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); @@ -107,7 +188,7 @@ namespace Barotrauma GameMain.GameSession?.Campaign != null && (GameMain.GameSession.Campaign.ShowCampaignUI || GameMain.GameSession.Campaign.ForceMapUI); - private static bool ShouldDrawInventory(Character character) + public static bool ShouldDrawInventory(Character character) { var controller = character.SelectedItem?.GetComponent(); @@ -159,7 +240,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { - UpdateBossHealthBars(deltaTime); + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) { @@ -623,7 +704,9 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; - if (character.FocusedCharacter.Info?.Title != null && !character.FocusedCharacter.Info.Title.IsNullOrEmpty()) + if (character.FocusedCharacter.Info?.Title != null && + !character.FocusedCharacter.Info.Title.IsNullOrEmpty() && + character.FocusedCharacter.TeamID != CharacterTeamType.Team1) { GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.Info.Title, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.Y += GUIStyle.SubHeadingFont.MeasureString(character.FocusedCharacter.Info.Title.Value).Y; @@ -661,27 +744,47 @@ namespace Barotrauma } } - public static void ShowBossHealthBar(Character character) + public static void ShowBossHealthBar(Character character, float damage) { if (character == null || character.IsDead || character.Removed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(character))) { return; } + AddBossProgressBar(new BossHealthBar(character)); + } + public static void ShowMissionProgressBar(Mission mission) + { + if (mission == null || mission.Completed || mission.Failed) { return; } + if (bossProgressBars.Any(b => b.IsDuplicate(mission))) { return; } + AddBossProgressBar(new MissionProgressBar(mission)); + } + + public static void ClearBossProgressBars() + { + for (int i = bossProgressBars.Count - 1; i>= 0; i--) + { + RemoveBossProgressBar(bossProgressBars[i]); + } + bossProgressBars.Clear(); + } + + private static void RemoveBossProgressBar(BossProgressBar progressBar) + { + progressBar.SideContainer.Parent?.RemoveChild(progressBar.SideContainer); + progressBar.TopContainer.Parent?.RemoveChild(progressBar.TopContainer); + bossProgressBars.Remove(progressBar); + } + + private static void AddBossProgressBar(BossProgressBar progressBar) + { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; if (healthBarMode == EnemyHealthBarMode.HideAll) { return; } - - var existingBar = bossHealthBars.Find(b => b.Character == character); - if (existingBar != null) + if (bossProgressBars.Count > 5) { - existingBar.FadeTimer = BossHealthBarDuration; - return; - } - - if (bossHealthBars.Count > 5) - { - BossHealthBar oldestHealthBar = bossHealthBars.First(); - foreach (var bar in bossHealthBars) + BossProgressBar oldestHealthBar = bossProgressBars.First(); + foreach (var bar in bossProgressBars) { if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) { @@ -690,62 +793,69 @@ namespace Barotrauma } oldestHealthBar.FadeTimer = Math.Min(oldestHealthBar.FadeTimer, 1.0f); } - - bossHealthBars.Add(new BossHealthBar(character)); + bossProgressBars.Add(progressBar); } - public static void UpdateBossHealthBars(float deltaTime) + public static void UpdateBossProgressBars(float deltaTime) { var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; - for (int i = 0; i < bossHealthBars.Count; i++) + for (int i = 0; i < bossProgressBars.Count; i++) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; - bool showTopBar = i == 0; - if (showTopBar != bossHealthBar.TopContainer.Visible) + bool showTopBar = i == bossProgressBars.Count - 1; + if (showTopBar && !bossHealthBar.TopContainer.Visible) { - bossHealthContainer.Recalculate(); + bossHealthBar.SideContainer.SetAsLastChild(); + SetColor(bossHealthBar, bossHealthBar.SideContainer, 0); } bossHealthBar.TopContainer.Visible = showTopBar; bossHealthBar.SideContainer.Visible = !bossHealthBar.TopContainer.Visible; - float health = bossHealthBar.Character.Vitality / bossHealthBar.Character.MaxVitality; - + bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = bossHealthBar.State; float alpha = Math.Min(bossHealthBar.FadeTimer, 1.0f); - foreach (var c in bossHealthBar.SideContainer.GetAllChildren().Concat(bossHealthBar.TopContainer.GetAllChildren())) + + if (bossHealthBar.TopContainer.Visible) { - c.Color = new Color(c.Color, (byte)(alpha * 255)); - if (c is GUITextBlock textBlock) + SetColor(bossHealthBar, bossHealthBar.TopContainer, alpha); + } + if (bossHealthBar.SideContainer.Visible) + { + SetColor(bossHealthBar, bossHealthBar.SideContainer, alpha); + } + + static void SetColor(BossProgressBar bossHealthBar, GUIComponent container, float alpha) + { + foreach (var component in container.GetAllChildren()) { - textBlock.TextColor = new Color(bossHealthBar.Character.IsDead ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + component.Color = new Color(bossHealthBar.Color, (byte)(alpha * 255)); + if (component is GUITextBlock textBlock) + { + textBlock.TextColor = new Color(bossHealthBar.Completed ? Color.Gray : textBlock.TextColor, (byte)(alpha * 255)); + } } } - bossHealthBar.TopHealthBar.BarSize = bossHealthBar.SideHealthBar.BarSize = health; - Color color = bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("poison") > 0 || bossHealthBar.Character.CharacterHealth.GetAfflictionStrength("paralysis") > 0 ? GUIStyle.HealthBarColorPoisoned : GUIStyle.Red; - bossHealthBar.TopHealthBar.Color = bossHealthBar.SideHealthBar.Color = color; - - if (bossHealthBar.Character.Removed || !bossHealthBar.Character.Enabled) + if (bossHealthBar.Interrupted) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 1.0f); } - else if (bossHealthBar.Character.IsDead) + else if (bossHealthBar.Completed) { bossHealthBar.FadeTimer = Math.Min(bossHealthBar.FadeTimer, 5.0f); } bossHealthBar.FadeTimer -= deltaTime; } - - for (int i = bossHealthBars.Count - 1; i >= 0 ; i--) + for (int i = bossProgressBars.Count - 1; i >= 0 ; i--) { - var bossHealthBar = bossHealthBars[i]; + var bossHealthBar = bossProgressBars[i]; if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); - bossHealthBars.RemoveAt(i); + bossProgressBars.RemoveAt(i); bossHealthContainer.Recalculate(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index c4bdcaba9..47d87e1ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -540,37 +540,45 @@ namespace Barotrauma string ragdollFile = inc.ReadString(); Identifier npcId = inc.ReadIdentifier(); + + Identifier factionId = inc.ReadIdentifier(); + float minReputationToHire = 0.0f; + if (factionId != default) + { + minReputationToHire = inc.ReadSingle(); + } + uint jobIdentifier = inc.ReadUInt32(); int variant = inc.ReadByte(); - JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (jobIdentifier > 0) - { + { jobPrefab = JobPrefab.Prefabs.Find(jp => jp.UintIdentifier == jobIdentifier); + if (jobPrefab == null) + { + throw new Exception($"Error while reading {nameof(CharacterInfo)} received from the server: could not find a job prefab with the identifier \"{jobIdentifier}\"."); + } foreach (SkillPrefab skillPrefab in jobPrefab.Skills.OrderBy(s => s.Identifier)) { float skillLevel = inc.ReadSingle(); skillLevels.Add(skillPrefab.Identifier, skillLevel); - } - } + } + } - // TODO: animations CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId) { - ID = infoID + ID = infoID, + MinReputationToHire = (factionId, minReputationToHire) }; ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); ch.Head.SkinColor = skinColor; ch.Head.HairColor = hairColor; ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); - if (ch.Job != null) - { - ch.Job.OverrideSkills(skillLevels); - } + ch.Job?.OverrideSkills(skillLevels); - ch.ExperiencePoints = inc.ReadUInt16(); + ch.ExperiencePoints = inc.ReadInt32(); ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 23db24f69..8139f283c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -593,6 +593,7 @@ namespace Barotrauma { character.MerchantIdentifier = inc.ReadIdentifier(); } + character.Faction = inc.ReadIdentifier(); character.HumanPrefabHealthMultiplier = humanPrefabHealthMultiplier; character.Wallet.Balance = balance; character.Wallet.RewardDistribution = rewardDistribution; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 6fe86291d..57a4c8015 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -87,6 +87,8 @@ namespace Barotrauma /// Container for the icons above the health bar /// private GUIComponent afflictionIconContainer; + private float afflictionIconRefreshTimer; + const float AfflictionIconRefreshInterval = 1.0f; private GUIButton showHiddenAfflictionsButton; @@ -861,7 +863,7 @@ namespace Barotrauma { treatmentButton.ToolTip = RichString.Rich( - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + $"‖color:gui.green‖[{PlayerInput.PrimaryMouseLabel}] " + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + treatmentButton.ToolTip.NestedStr); } @@ -1157,15 +1159,20 @@ namespace Barotrauma } } - afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + afflictionIconRefreshTimer -= deltaTime; + if (afflictionIconRefreshTimer <= 0.0f) { - if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } - if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } - var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); - var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); - return index1.CompareTo(index2); - }); - (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => + { + if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } + if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } + var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); + var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); + return index1.CompareTo(index2); + }); + (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; + afflictionIconRefreshTimer = AfflictionIconRefreshInterval; + } Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) @@ -1983,6 +1990,7 @@ namespace Barotrauma { newAfflictions.Clear(); newPeriodicEffects.Clear(); + bool newAdded = false; byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -2062,6 +2070,7 @@ namespace Barotrauma { existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction, limb); + newAdded = true; } existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) @@ -2088,6 +2097,11 @@ namespace Barotrauma CalculateVitality(); DisplayedVitality = Vitality; + + if (newAdded) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } partial void UpdateSkinTint() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 5fe12e73e..913f15dca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -554,7 +554,7 @@ namespace Barotrauma float damage = 0; foreach (var affliction in result.Afflictions) { - if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.DamageParticles && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { damage += affliction.GetVitalityDecrease(null); } @@ -563,11 +563,11 @@ namespace Barotrauma float bleedingDamageMultiplier = 1; foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { - if (damageModifier.MatchesAfflictionType("damage")) + if (damageModifier.MatchesAfflictionType(AfflictionPrefab.DamageType)) { damageMultiplier *= damageModifier.DamageMultiplier; } - else if (damageModifier.MatchesAfflictionType("bleeding")) + else if (damageModifier.MatchesAfflictionType(AfflictionPrefab.BleedingType)) { bleedingDamageMultiplier *= damageModifier.DamageMultiplier; } @@ -599,7 +599,7 @@ namespace Barotrauma { if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) { - overrideParticle = GameMain.ParticleManager?.FindPrefab(damageModifier.DamageParticle); + overrideParticle = ParticleManager.FindPrefab(damageModifier.DamageParticle); break; } } @@ -646,7 +646,7 @@ namespace Barotrauma dripParticleTimer += wetTimer * deltaTime * Mass * (wetTimer > 0.9f ? 50.0f : 5.0f); if (dripParticleTimer > 1.0f) { - float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.width, body.height) : body.radius; + float dropRadius = body.BodyShape == PhysicsBody.Shape.Rectangle ? Math.Min(body.Width, body.Height) : body.Radius; GameMain.ParticleManager.CreateParticle( "waterdrop", WorldPosition + Rand.Vector(Rand.Range(0.0f, ConvertUnits.ToDisplayUnits(dropRadius))), @@ -683,10 +683,10 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { - float brightness = Math.Max(1.0f - burnOverLayStrength, 0.2f); var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } - + float burn = spriteParams.IgnoreTint ? 0 : burnOverLayStrength; + float brightness = Math.Max(1.0f - burn, 0.2f); Color clr = spriteParams.Color; if (!spriteParams.IgnoreTint) { @@ -727,7 +727,7 @@ namespace Barotrauma } } - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index f6a770b46..c839987fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -649,7 +649,7 @@ namespace Barotrauma { if (Submarine.MainSub == null) { return; } MapEntity.SelectedList.Clear(); - MapEntity.mapEntityList.ForEach(me => me.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); WikiImage.Create(Submarine.MainSub); })); @@ -762,7 +762,7 @@ namespace Barotrauma state = !GameMain.LightManager.LosEnabled; } GameMain.LightManager.LosEnabled = state; - NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("los", false); @@ -773,7 +773,7 @@ namespace Barotrauma state = !GameMain.LightManager.LightingEnabled; } GameMain.LightManager.LightingEnabled = state; - NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.White); + NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("lighting|lights", false); @@ -791,7 +791,7 @@ namespace Barotrauma hull.OriginalAmbientLight = null; } } - NewMessage("Restored all hull ambient lights", Color.White); + NewMessage("Restored all hull ambient lights", Color.Yellow); return; } @@ -813,11 +813,11 @@ namespace Barotrauma if (add) { - NewMessage($"Set ambient light color to {color}.", Color.White); + NewMessage($"Set ambient light color to {color}.", Color.Yellow); } else { - NewMessage($"Increased ambient light by {color}.", Color.White); + NewMessage($"Increased ambient light by {color}.", Color.Yellow); } }); AssignRelayToServer("ambientlight", false); @@ -1134,7 +1134,7 @@ namespace Barotrauma state = !GameMain.DebugDraw; } GameMain.DebugDraw = state; - NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1167,7 +1167,7 @@ namespace Barotrauma state = !TextManager.DebugDraw; } TextManager.DebugDraw = state; - NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.Yellow); }); AssignRelayToServer("debugdraw", false); @@ -1180,19 +1180,19 @@ namespace Barotrauma var config = GameSettings.CurrentConfig; config.Audio.DisableVoiceChatFilters = state; GameSettings.SetCurrentConfig(config); - NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.Yellow); }); AssignRelayToServer("togglevoicechatfilters", false); commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) => { GameMain.ShowFPS = !GameMain.ShowFPS; - NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("showperf", "showperf: Toggle performance statistics on/off.", (string[] args) => { GameMain.ShowPerf = !GameMain.ShowPerf; - NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.White); + NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.Yellow); })); AssignOnClientExecute("netstats", (string[] args) => @@ -1204,55 +1204,55 @@ namespace Barotrauma commands.Add(new Command("hudlayoutdebugdraw|debugdrawhudlayout", "hudlayoutdebugdraw: Toggle the debug drawing mode of HUD layout areas on/off.", (string[] args) => { HUDLayoutSettings.DebugDraw = !HUDLayoutSettings.DebugDraw; - NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.White); + NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.Yellow); })); commands.Add(new Command("interactdebugdraw|debugdrawinteract", "interactdebugdraw: Toggle the debug drawing mode of item interaction ranges on/off.", (string[] args) => { Character.DebugDrawInteract = !Character.DebugDrawInteract; - NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.White); + NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.Yellow); }, isCheat: true)); AssignOnExecute("togglehud|hud", (string[] args) => { GUI.DisableHUD = !GUI.DisableHUD; GameMain.Instance.IsMouseVisible = !GameMain.Instance.IsMouseVisible; - NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.White); + NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.Yellow); }); AssignRelayToServer("togglehud|hud", false); AssignOnExecute("toggleupperhud", (string[] args) => { GUI.DisableUpperHUD = !GUI.DisableUpperHUD; - NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.White); + NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.Yellow); }); AssignRelayToServer("toggleupperhud", false); AssignOnExecute("toggleitemhighlights", (string[] args) => { GUI.DisableItemHighlights = !GUI.DisableItemHighlights; - NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.White); + NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.Yellow); }); AssignRelayToServer("toggleitemhighlights", false); AssignOnExecute("togglecharacternames", (string[] args) => { GUI.DisableCharacterNames = !GUI.DisableCharacterNames; - NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.White); + NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.Yellow); }); AssignRelayToServer("togglecharacternames", false); AssignOnExecute("followsub", (string[] args) => { Camera.FollowSub = !Camera.FollowSub; - NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.White); + NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.Yellow); }); AssignRelayToServer("followsub", false); AssignOnExecute("toggleaitargets|aitargets", (string[] args) => { AITarget.ShowAITargets = !AITarget.ShowAITargets; - NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.White); + NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.Yellow); }); AssignRelayToServer("toggleaitargets|aitargets", false); @@ -1274,10 +1274,36 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosAlpha = 1f; } - NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.White); + NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); }); AssignRelayToServer("debugai", false); + AssignOnExecute("showmonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = true; + CreatureMetrics.Save(); + NewMessage("All monsters are now visible in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("showmonsters", false); + + AssignOnExecute("hidemonsters", (string[] args) => + { + CreatureMetrics.UnlockAll = false; + CreatureMetrics.Save(); + NewMessage("All monsters that haven't yet been encountered in the game are now hidden in the character editor.", Color.Yellow); + if (Screen.Selected == GameMain.CharacterEditorScreen) + { + GameMain.CharacterEditorScreen.Deselect(); + GameMain.CharacterEditorScreen.Select(); + } + }); + AssignRelayToServer("hidemonsters", false); + AssignRelayToServer("water|editwater", false); AssignRelayToServer("fire|editfire", false); @@ -3001,7 +3027,7 @@ namespace Barotrauma ThrowError($"Could not find the location type \"{args[0]}\"."); return; } - GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(locationType); + GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(GameMain.GameSession.Campaign, locationType); }, () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 18e93bfbd..0d7bb2e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -83,6 +83,7 @@ namespace Barotrauma GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), selectedButton: null); // gray out the last text block if (conversationList.Content.Children.LastOrDefault() is GUILayoutGroup lastElement) { @@ -269,14 +270,7 @@ namespace Barotrauma if (actionInstance != null) { actionInstance.selectedOption = selectedOption; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); btn.ExternalHighlight = true; return true; } @@ -286,14 +280,7 @@ namespace Barotrauma SendResponse(actionId.Value, selectedOption); btn.CanBeFocused = false; btn.ExternalHighlight = true; - foreach (GUIButton otherButton in optionButtons) - { - otherButton.CanBeFocused = false; - if (otherButton != btn) - { - otherButton.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); - } - } + DisableButtons(optionButtons, btn); return true; } //should not happen @@ -305,6 +292,18 @@ namespace Barotrauma } } + public static void SelectOption(ushort actionId, int option) + { + if (lastMessageBox.UserData is Pair userData) + { + if (userData.Second != actionId) { return; } + + GUIListBox conversationList = lastMessageBox.FindChild("conversationlist", true) as GUIListBox; + Debug.Assert(conversationList != null); + DisableButtons(conversationList.Content.GetAllChildren(), (btn) => btn.UserData is int i && i == option); + } + } + private static Tuple GetSizes(DialogTypes dialogTypes) { return dialogTypes switch @@ -383,6 +382,30 @@ namespace Barotrauma return buttons; } + private static void DisableButtons(IEnumerable buttons, GUIButton selectedButton) + { + DisableButtons(buttons, (btn) => btn == selectedButton); + } + + private static void DisableButtons(IEnumerable buttons, Func isSelectedButton) + { + foreach (GUIButton btn in buttons) + { + if (btn.CanBeFocused) + { + btn.CanBeFocused = false; + if (isSelectedButton(btn)) + { + btn.Selected = true; + } + else + { + btn.TextBlock.OverrideTextColor(Color.DarkGray * 0.8f); + } + } + } + } + private static void SendResponse(UInt16 actionId, int selectedOption) { IWriteMessage outmsg = new WriteOnlyMessage(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 9093450ec..ef02adad7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -608,58 +608,90 @@ namespace Barotrauma } break; case NetworkEventType.CONVERSATION: - UInt16 identifier = msg.ReadUInt16(); - string eventSprite = msg.ReadString(); - byte dialogType = msg.ReadByte(); - bool continueConversation = msg.ReadBoolean(); - UInt16 speakerId = msg.ReadUInt16(); - string text = msg.ReadString(); - bool fadeToBlack = msg.ReadBoolean(); - byte optionCount = msg.ReadByte(); - List options = new List(); - for (int i = 0; i < optionCount; i++) { - options.Add(msg.ReadString()); - } - - byte endCount = msg.ReadByte(); - int[] endings = new int[endCount]; - for (int i = 0; i < endCount; i++) - { - endings[i] = msg.ReadByte(); - } - - if (string.IsNullOrEmpty(text) && optionCount == 0) - { - GUIMessageBox.MessageBoxes.ForEachMod(mb => + UInt16 identifier = msg.ReadUInt16(); + string eventSprite = msg.ReadString(); + byte dialogType = msg.ReadByte(); + bool continueConversation = msg.ReadBoolean(); + UInt16 speakerId = msg.ReadUInt16(); + string text = msg.ReadString(); + bool fadeToBlack = msg.ReadBoolean(); + byte optionCount = msg.ReadByte(); + List options = new List(); + for (int i = 0; i < optionCount; i++) { - if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + options.Add(msg.ReadString()); + } + + byte endCount = msg.ReadByte(); + int[] endings = new int[endCount]; + for (int i = 0; i < endCount; i++) + { + endings[i] = msg.ReadByte(); + } + + if (string.IsNullOrEmpty(text) && optionCount == 0) + { + GUIMessageBox.MessageBoxes.ForEachMod(mb => { - (mb as GUIMessageBox)?.Close(); - } - }); + if (mb.UserData is Pair pair && pair.First == "ConversationAction" && pair.Second == identifier) + { + (mb as GUIMessageBox)?.Close(); + } + }); + } + else + { + ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + } + if (Entity.FindEntityByID(speakerId) is Character speaker) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); + } + break; } - else + case NetworkEventType.CONVERSATION_SELECTED_OPTION: { - ConversationAction.CreateDialog(text, Entity.FindEntityByID(speakerId) as Character, options, endings, eventSprite, identifier, fadeToBlack, (ConversationAction.DialogTypes)dialogType, continueConversation); + UInt16 identifier = msg.ReadUInt16(); + int selectedOption = msg.ReadByte() - 1; + ConversationAction.SelectOption(identifier, selectedOption); + break; } - if (Entity.FindEntityByID(speakerId) is Character speaker) - { - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.SetCustomInteract(null, null); - } - break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); + int locationIndex = msg.ReadInt32(); + int destinationIndex = msg.ReadInt32(); + string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; + if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) + { + Location location = map.Locations[locationIndex]; + map.Discover(location, checkTalents: false); + + LocationConnection? connection = null; + if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) + { + Location destination = map.Locations[destinationIndex]; + connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); + } + if (connection != null) + { + location.UnlockMission(prefab, connection); + } + else + { + location.UnlockMission(prefab); + } + } } break; case NetworkEventType.UNLOCKPATH: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index fcd12b072..fa15748e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -8,7 +8,7 @@ namespace Barotrauma public override int State { get { return base.State; } - protected set + set { if (state != value) { @@ -45,7 +45,10 @@ namespace Barotrauma { requireRescue.Add(character); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(character); + } #endif } ushort itemCount = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 08356c60c..1a9941f42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -10,8 +10,7 @@ namespace Barotrauma public override RichString GetMissionRewardText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - + LocalizedString rewardText = GetRewardAmountText(sub); LocalizedString retVal; if (rewardPerCrate.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..5becd9ba2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -0,0 +1,138 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override bool DisplayAsCompleted => false; + + public override bool DisplayAsFailed => false; + + partial void OnStateChangedProjSpecific() + { + SoundPlayer.ForceMusicUpdate(); + if (Phase == MissionPhase.NoItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 8, fadeOut: false, startZoom: 1.0f, endZoom: 0.3f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = 3.0f + }; + } + }, delay: 3.0f); + } + else if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.StartCoroutine(wakeUpCoroutine(), name: "EndMission.wakeUpCoroutine"); + } + else if (Phase == MissionPhase.BossKilled) + { + if (!string.IsNullOrEmpty(endCinematicSound)) + { + SoundPlayer.PlaySound(endCinematicSound); + } + CoroutineManager.Invoke(() => + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = float.PositiveInfinity + }; + }, delay: 3.0f); + } + + IEnumerable wakeUpCoroutine() + { + yield return new WaitForSeconds(wakeUpCinematicDelay); + if (boss != null && !boss.Removed) + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 5.0f, fadeOut: false, losFadeIn: false, startZoom: 1.0f, endZoom: 0.4f * GUI.yScale) + { + RunWhilePaused = false, + EndWaitDuration = cameraWaitDuration + }; + } + yield return new WaitForSeconds(bossWakeUpDelay); + if (boss != null && !boss.Removed) + { + foreach (var limb in boss.AnimController.Limbs) + { + if (!limb.FreezeBlinkState) { continue; } + limb.FreezeBlinkState = false; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = true; + } + } + } + } + } + + partial void UpdateProjSpecific() + { + if (boss == null || boss.Removed) { return; } + if (Phase is MissionPhase.Initial or MissionPhase.NoItemsDestroyed or MissionPhase.SomeItemsDestroyed) + { + // Put asleep. + // Have to set the light every frame (or at least periodically), because light.Enabled is changed when Character.IsVisible changes (off/on screen). See GameScreen.Draw(). + foreach (var limb in boss.AnimController.Limbs) + { + if (limb.Params.BlinkFrequency > 0) + { + limb.FreezeBlinkState = true; + limb.BlinkPhase = -limb.Params.BlinkHoldTime; + if (limb.LightSource is Lights.LightSource light) + { + light.Enabled = false; + } + } + } + } + +#if DEBUG + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.O)) + { + State = 0; + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Y)) + { + destructibleItems.ForEach(it => it.Condition = 0.0f); + } + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.U)) + { + boss?.SetAllDamage(20000.0f, 0.0f, 0.0f); + } +#endif + } + + public override void ClientReadInitial(IReadMessage msg) + { + base.ClientReadInitial(msg); + + boss = Character.ReadSpawnData(msg); + + byte minionCount = msg.ReadByte(); + List minionList = new List(); + for (int i = 0; i < minionCount; i++) + { + var minion = Character.ReadSpawnData(msg); + if (minion == null) + { + throw new System.Exception($"Error in EndMission.ClientReadInitial: failed to create a minion (mission: {Prefab.Identifier}, index: {i})"); + } + minionList.Add(minion); + } + minions = minionList.ToImmutableArray(); + if (minions.Length != minionCount) + { + throw new System.Exception("Error in EndMission.ClientReadInitial: minion count does not match the server count (" + minionCount + " != " + minions.Length + "mission: " + Prefab.Identifier + ")"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs index 6036c0586..a7a45837c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/GoToMission.cs @@ -2,7 +2,7 @@ { partial class GoToMission : Mission { - public override bool DisplayAsCompleted => false; + public override bool DisplayAsCompleted => State >= Prefab.MaxProgressState; public override bool DisplayAsFailed => false; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 28eafbc6c..4a0e51172 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -32,44 +32,73 @@ namespace Barotrauma return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public virtual RichString GetMissionRewardText(Submarine sub) + /// + /// Returns the amount of marks you get from the reward (e.g. "3,000 mk") + /// + protected LocalizedString GetRewardAmountText(Submarine sub) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); + int baseReward = GetReward(sub); + int finalReward = GetFinalReward(sub); + string rewardAmountText = string.Format(CultureInfo.InvariantCulture, "{0:N0}", baseReward); + if (finalReward > baseReward) + { + rewardAmountText += $" + {string.Format(CultureInfo.InvariantCulture, "{0:N0}", finalReward - baseReward)}"; + } + return TextManager.GetWithVariable("currencyformat", "[credits]", rewardAmountText); } - public RichString GetReputationRewardText(Location currLocation) + /// + /// Returns the full reward text of the mission (e.g. "Reward: 2,000 mk" or "Reward: 500 mk x 2 (out of max 5) = 1,000 mk") + /// + public virtual RichString GetMissionRewardText(Submarine sub) + { + LocalizedString rewardText = GetRewardAmountText(sub); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖" + rewardText + "‖end‖")); + } + + public RichString GetReputationRewardText() { List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - LocalizedString name = ""; - - if (reputationReward.Key == "location") + FactionPrefab targetFactionPrefab; + if (reputationReward.Key == "location" ) { - name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; + targetFactionPrefab = OriginLocation.Faction?.Prefab; } else { - var faction = FactionPrefab.Prefabs.Find(f => f.Identifier == reputationReward.Key); - if (faction != null) - { - name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; - } - else - { - name = TextManager.Get(reputationReward.Key); - } + FactionPrefab.Prefabs.TryGet(reputationReward.Key, out targetFactionPrefab); + } + + if (targetFactionPrefab == null) + { + return string.Empty; } - float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); - string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers + + float totalReputationChange = reputationReward.Value; + if (GameMain.GameSession?.Campaign?.Factions.Find(f => f.Prefab == targetFactionPrefab) is Faction faction) + { + totalReputationChange = reputationReward.Value * faction.Reputation.GetReputationChangeMultiplier(reputationReward.Value); + } + + LocalizedString name = $"‖color:{XMLExtensions.ToStringHex(targetFactionPrefab.IconColor)}‖{targetFactionPrefab.Name}‖end‖"; + float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, totalReputationChange); + string formattedValue = ((int)Math.Round(totalReputationChange)).ToString("+#;-#;0"); //force plus sign for positive numbers LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", ("[reputationname]", name), - ("[reputationvalue]", $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + ("[reputationvalue]", $"‖color:{XMLExtensions.ToStringHex(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); reputationRewardTexts.Add(rewardText.Value); } - return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + if (reputationRewardTexts.Any()) + { + return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); + } + else + { + return string.Empty; + } } partial void ShowMessageProjSpecific(int missionState) @@ -107,6 +136,11 @@ namespace Barotrauma }; } + public Identifier GetOverrideMusicType() + { + return Prefab.GetOverrideMusicType(State); + } + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 3ec2386cf..b360f67ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -8,6 +8,7 @@ namespace Barotrauma { foreach (Mission mission in missions) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox(RichString.Rich(mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index c8172bfaa..0bddcc04f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -1,11 +1,16 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class MissionPrefab : PrefabWithUintIdentifier { + private ImmutableArray portraits = new ImmutableArray(); + + public bool HasPortraits => portraits.Length > 0; + public Sprite Icon { get; @@ -49,24 +54,57 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; + private ImmutableDictionary overrideMusicOnState; + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); + Dictionary overrideMusic = new Dictionary(); + List portraits = new List(); foreach (var subElement in element.Elements()) { - string name = subElement.Name.ToString(); - if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + switch (subElement.Name.ToString().ToLowerInvariant()) { - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); - } - else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) - { - hudIcon = new Sprite(subElement); - hudIconColor = subElement.GetAttributeColor("color"); + case "icon": + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + break; + case "hudicon": + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + break; + case "overridemusic": + overrideMusic.Add( + subElement.GetAttributeInt("state", 0), + subElement.GetAttributeIdentifier("type", Identifier.Empty)); + break; + case "portrait": + var portrait = new Sprite(subElement, lazyLoad: true); + if (portrait != null) + { + portraits.Add(portrait); + } + break; } } + this.portraits = portraits.ToImmutableArray(); + overrideMusicOnState = overrideMusic.ToImmutableDictionary(); + } + + public Identifier GetOverrideMusicType(int state) + { + if (overrideMusicOnState.TryGetValue(state, out Identifier id)) + { + return id; + } + return Identifier.Empty; + } + + public Sprite GetPortrait(int randomSeed) + { + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } partial void DisposeProjectSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs index 3033769f1..d964fadc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MonsterMission.cs @@ -13,11 +13,12 @@ namespace Barotrauma byte monsterCount = msg.ReadByte(); for (int i = 0; i < monsterCount; i++) { - monsters.Add(Character.ReadSpawnData(msg)); - } - if (monsters.Contains(null)) - { - throw new System.Exception("Error in MonsterMission.ClientReadInitial: monster list contains null (mission: " + Prefab.Identifier + ")"); + var monster = Character.ReadSpawnData(msg); + if (monster == null) + { + throw new System.Exception($"Error in MonsterMission.ClientReadInitial: failed to create a monster (mission: {Prefab.Identifier}, index: {i})"); + } + monsters.Add(monster); } if (monsters.Count != monsterCount) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 12c80c496..be7a49430 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -11,38 +11,59 @@ namespace Barotrauma public override void ClientReadInitial(IReadMessage msg) { base.ClientReadInitial(msg); - bool usedExistingItem = msg.ReadBoolean(); - if (usedExistingItem) + + foreach (var target in targets) { - ushort id = msg.ReadUInt16(); - item = Entity.FindEntityByID(id) as Item; - if (item == null) + bool targetFound = msg.ReadBoolean(); + if (!targetFound) { continue; } + + bool usedExistingItem = msg.ReadBoolean(); + if (usedExistingItem) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + ushort id = msg.ReadUInt16(); + target.Item = Entity.FindEntityByID(id) as Item; + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + } + } + else + { + target.Item = Item.ReadSpawnData(msg); + if (target.Item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + } + } + + int executedEffectCount = msg.ReadByte(); + for (int i = 0; i < executedEffectCount; i++) + { + int listIndex = msg.ReadByte(); + int effectIndex = msg.ReadByte(); + var selectedEffect = target.StatusEffects[listIndex][effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); + } + + if (target.Item.body != null) + { + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; } } - else + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + int targetCount = msg.ReadByte(); + for (int i = 0; i < targetCount; i++) { - item = Item.ReadSpawnData(msg); - if (item == null) + var state = (Target.RetrievalState)msg.ReadByte(); + if (i < targets.Count) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + targets[i].State = state; } } - - int executedEffectCount = msg.ReadByte(); - for (int i = 0; i < executedEffectCount; i++) - { - int index1 = msg.ReadByte(); - int index2 = msg.ReadByte(); - var selectedEffect = statusEffects[index1][index2]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); - } - - if (item.body != null) - { - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 85afac3c9..5af8c6e02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -3,7 +3,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; @@ -29,6 +28,8 @@ namespace Barotrauma private Point resolutionWhenCreated; + private bool needsHireableRefresh; + private enum SortingMethod { AlphabeticalAsc, @@ -50,6 +51,8 @@ namespace Barotrauma campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting( "CrewManagement.UpdateLocationView".ToIdentifier(), (locationChangeInfo) => UpdateLocationView(locationChangeInfo.NewLocation, true, locationChangeInfo.PrevLocation)); + Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting( + "CrewManagement.UpdateLocationView".ToIdentifier(), _ => needsHireableRefresh = true); } public void RefreshPermissions() @@ -68,7 +71,13 @@ namespace Barotrauma { if (child.FindChild(c => c is GUIButton && c.UserData is CharacterInfo, true) is GUIButton buyButton) { - buyButton.Enabled = HasPermission; + CharacterInfo characterInfo = buyButton.UserData as CharacterInfo; + bool enoughReputationToHire = EnoughReputationToHire(characterInfo); + buyButton.Enabled = HasPermission && enoughReputationToHire; + foreach (GUITextBlock text in child.GetAllChildren()) + { + text.TextColor = new Color(text.TextColor, buyButton.Enabled ? 1.0f : 0.6f); + } } } } @@ -294,18 +303,21 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - String.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? + string.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } @@ -313,9 +325,26 @@ namespace Barotrauma { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => + CompareReputationRequirement(x.GUIComponent, y.GUIComponent) ?? ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } + + int? CompareReputationRequirement(GUIComponent c1, GUIComponent c2) + { + CharacterInfo info1 = ((InfoSkill)c1.UserData).CharacterInfo; + CharacterInfo info2 = ((InfoSkill)c2.UserData).CharacterInfo; + float requirement1 = EnoughReputationToHire(info1) ? 0 : info1.MinReputationToHire.reputation; + float requirement2 = EnoughReputationToHire(info2) ? 0 : info2.MinReputationToHire.reputation; + if (MathUtils.NearlyEqual(requirement1, 0.0f) && MathUtils.NearlyEqual(requirement2, 0.0f)) + { + return null; + } + else + { + return requirement1.CompareTo(requirement2); + } + } } private readonly struct InfoSkill @@ -367,12 +396,25 @@ namespace Barotrauma nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) + characterInfo.Title ?? characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; - jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); - + if (!characterInfo.MinReputationToHire.factionId.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == characterInfo.MinReputationToHire.factionId); + if (faction != null) + { + jobBlock.TextColor = faction.Prefab.IconColor; + } + } + var fullJobText = jobBlock.Text; + jobBlock.Text = ToolBox.LimitString(fullJobText, jobBlock.Font, jobBlock.Rect.Width); + if (jobBlock.Text != fullJobText) + { + jobBlock.ToolTip = fullJobText; + jobBlock.CanBeFocused = true; + } float width = 0.6f / 3; if (characterInfo.Job != null && skill != null) { @@ -410,7 +452,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => AddPendingHire(o as CharacterInfo) }; hireButton.OnAddedToGUIUpdateList += (GUIComponent btn) => @@ -426,10 +468,9 @@ namespace Barotrauma else if (!btn.Enabled) { btn.ToolTip = string.Empty; - btn.Enabled = HasPermission; + btn.Enabled = CanHire(characterInfo); } }; - } else if (listBox == pendingList) { @@ -437,7 +478,7 @@ namespace Barotrauma { ClickSound = GUISoundType.Cart, UserData = characterInfo, - Enabled = HasPermission, + Enabled = CanHire(characterInfo), OnClicked = (b, o) => RemovePendingHire(o as CharacterInfo) }; } @@ -474,12 +515,30 @@ namespace Barotrauma size = new Point(3 * mainGroup.AbsoluteSpacing + icon.Rect.Width + nameAndJobGroup.Rect.Width, mainGroup.Rect.Height); new GUIButton(new RectTransform(size, frame.RectTransform) { RelativeOffset = new Vector2(0.025f) }, style: null) { - Enabled = HasPermission, - ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), + Enabled = CanHire(characterInfo), + ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", PlayerInput.PrimaryMouseLabel), UserData = characterInfo, OnClicked = CreateRenamingComponent }; } + + bool CanHire(CharacterInfo characterInfo) + { + if (!HasPermission) { return false; } + return EnoughReputationToHire(characterInfo); + } + } + + private bool EnoughReputationToHire(CharacterInfo characterInfo) + { + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } + return true; } private void CreateCharacterPreviewFrame(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -488,13 +547,13 @@ namespace Barotrauma Point absoluteOffset = new Point( pivot == Pivot.TopLeft ? listBox.Parent.Parent.Rect.Right + 5 : listBox.Parent.Parent.Rect.Left - 5, characterFrame.Rect.Top); - int frameSize = (int)(GUI.Scale * 300); - if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize) < 0) + Point frameSize = new Point(GUI.IntScale(300), GUI.IntScale(350)); + if (GameMain.GraphicsHeight - (absoluteOffset.Y + frameSize.Y) < 0) { pivot = listBox == hireableList ? Pivot.BottomLeft : Pivot.BottomRight; absoluteOffset.Y = characterFrame.Rect.Bottom; } - characterPreviewFrame = new GUIFrame(new RectTransform(new Point(frameSize), parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) + characterPreviewFrame = new GUIFrame(new RectTransform(frameSize, parent: campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Parent.RectTransform, pivot: pivot) { AbsoluteOffset = absoluteOffset }, style: "InnerFrame") @@ -503,7 +562,8 @@ namespace Barotrauma }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), characterPreviewFrame.RectTransform, anchor: Anchor.Center)) { - RelativeSpacing = 0.01f + RelativeSpacing = 0.01f, + Stretch = true }; // Character info @@ -545,9 +605,23 @@ namespace Barotrauma blockHeight = 1.0f / characterSkills.Count(); foreach (Skill skill in characterSkills) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillNameGroup.RectTransform), TextManager.Get("SkillName." + skill.Identifier), font: GUIStyle.SmallFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), skillLevelGroup.RectTransform), ((int)skill.Level).ToString(), textAlignment: Alignment.Right); } + + if (characterInfo.MinReputationToHire.reputation > 0.0f) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)characterInfo.MinReputationToHire.reputation).ToString()), + ("[faction]", TextManager.Get("faction." + characterInfo.MinReputationToHire.factionId).Value)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), mainGroup.RectTransform), + repStr, textColor: !EnoughReputationToHire(characterInfo) ? GUIStyle.Orange : GUIStyle.Green, + font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center); + } + mainGroup.Recalculate(); + characterPreviewFrame.RectTransform.MinSize = + new Point(0, (int)(mainGroup.Children.Sum(c => c.Rect.Height + mainGroup.Rect.Height * mainGroup.RelativeSpacing) / mainGroup.RectTransform.RelativeSize.Y)); } private bool SelectCharacter(GUIListBox listBox, GUIFrame characterFrame, CharacterInfo characterInfo) @@ -636,7 +710,7 @@ namespace Barotrauma List nonDuplicateHires = new List(); hires.ForEach(hireInfo => { - if(campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) + if (campaign.CrewManager.GetCharacterInfos().None(crewInfo => crewInfo.IsNewHire && crewInfo.GetIdentifierUsingOriginalName() == hireInfo.GetIdentifierUsingOriginalName())) { nonDuplicateHires.Add(hireInfo); } @@ -791,6 +865,16 @@ namespace Barotrauma playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement); } + if (needsHireableRefresh) + { + RefreshCrewFrames(hireableList); + if (sortingDropDown?.SelectedItemData != null) + { + SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); + } + needsHireableRefresh = false; + } + (GUIComponent highlightedFrame, CharacterInfo highlightedInfo) = FindHighlightedCharacter(GUI.MouseOn); if (highlightedFrame != null && highlightedInfo != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 9a84a1b59..641c6e3aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -106,6 +106,11 @@ namespace Barotrauma public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + /// + /// A horizontal scaling factor for low aspect ratios (small width relative to height) + /// + public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f; + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; public static int UIWidth @@ -140,13 +145,20 @@ namespace Barotrauma public static Texture2D WhiteTexture => solidWhiteTexture; private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite; - private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; - private static int debugDrawMetadataOffset; - private static readonly string[] ignoredMetadataInfo = { string.Empty, string.Empty, string.Empty, string.Empty }; + private static bool debugDrawSounds, debugDrawEvents; + + private static DebugDrawMetaData debugDrawMetaData; + + public struct DebugDrawMetaData + { + public bool Enabled; + public bool FactionMetadata, UpgradeLevels, UpgradePrices; + public int Offset; + } public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice; - private static List messages = new List(); + private static readonly List messages = new List(); public static GUIFrame PauseMenu { get; private set; } public static GUIFrame SettingsMenuContainer { get; private set; } @@ -195,8 +207,9 @@ namespace Barotrauma SettingsMenuOpen || DebugConsole.IsOpen || GameSession.IsTabMenuOpen || - (GameMain.GameSession?.GameMode?.Paused ?? false) || - CharacterHUD.IsCampaignInterfaceOpen; + GameMain.GameSession?.GameMode is { Paused: true } || + CharacterHUD.IsCampaignInterfaceOpen || + GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } }; } } @@ -533,18 +546,17 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { // TODO: TEST THIS - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { string text = "Ctrl+M to hide campaign metadata debug info\n\n" + - $"Ctrl+1 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[0]) ? "hide" : "show")} outpost reputations, \n" + - $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + - $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + - $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; + $"Ctrl+1 to {(debugDrawMetaData.FactionMetadata ? "hide" : "show")} faction reputations, \n" + + $"Ctrl+2 to {(debugDrawMetaData.UpgradeLevels ? "hide" : "show")} upgrade levels, \n" + + $"Ctrl+3 to {(debugDrawMetaData.UpgradePrices ? "hide" : "show")} upgrade prices"; Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = new Vector2(GameMain.GraphicsWidth - (textSize.X + 10), 300); DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); pos.Y += textSize.Y + 8; - campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); + campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, campaignMode, debugDrawMetaData); } else { @@ -684,37 +696,24 @@ namespace Barotrauma } } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float aberrationStrength = 1.0f) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None) { - double aberrationT = (Timing.TotalTime * 0.5f); - GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); - GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * - (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); - - Matrix.CreateOrthographicOffCenter(0, GameMain.GraphicsWidth, GameMain.GraphicsHeight, 0, 0, -1, out Matrix projection); - - GameMain.GameScreen.PostProcessEffect.Parameters["MatrixTransform"].SetValue(projection); - GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; - GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); - - spriteBatch.Begin(SpriteSortMode.Immediate, effect: GameMain.GameScreen.PostProcessEffect); + Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, - (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; - float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; - float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; + (float)area.Width / backgroundSprite.SourceRect.Width, + (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f; + float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width; + float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height; - double noiseT = (Timing.TotalTime * 0.02f); + double noiseT = Timing.TotalTime * 0.02f; Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); spriteBatch.Draw(backgroundSprite.Texture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, backgroundSprite.size / 2, - scale, SpriteEffects.None, 0.0f); - - spriteBatch.End(); + area.Center.ToVector2() + pos, + null, color, 0.0f, backgroundSprite.size / 2, + scale, spriteEffects, 0.0f); } #region Update list @@ -1206,48 +1205,37 @@ namespace Barotrauma } if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M)) { - debugDrawMetadata = !debugDrawMetadata; + debugDrawMetaData.Enabled = !debugDrawMetaData.Enabled; } - if (debugDrawMetadata) + if (debugDrawMetaData.Enabled) { if (PlayerInput.KeyHit(Keys.Up)) { - debugDrawMetadataOffset--; + debugDrawMetaData.Offset--; } - if (PlayerInput.KeyHit(Keys.Down)) { - debugDrawMetadataOffset++; + debugDrawMetaData.Offset++; } - if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.D1)) { - ignoredMetadataInfo[0] = ignoredMetadataInfo[0] == string.Empty ? "reputation.location" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.FactionMetadata = !debugDrawMetaData.FactionMetadata; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D2)) { - ignoredMetadataInfo[1] = ignoredMetadataInfo[1] == string.Empty ? "reputation.faction" : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradeLevels = !debugDrawMetaData.UpgradeLevels; + debugDrawMetaData.Offset = 0; } - if (PlayerInput.KeyHit(Keys.D3)) { - ignoredMetadataInfo[2] = ignoredMetadataInfo[2] == string.Empty ? "upgrade." : string.Empty; - debugDrawMetadataOffset = 0; - } - - if (PlayerInput.KeyHit(Keys.D4)) - { - ignoredMetadataInfo[3] = ignoredMetadataInfo[3] == string.Empty ? "upgradeprice." : string.Empty; - debugDrawMetadataOffset = 0; + debugDrawMetaData.UpgradePrices = !debugDrawMetaData.UpgradePrices; + debugDrawMetaData.Offset = 0; } } - } HandlePersistingElements(deltaTime); @@ -2599,8 +2587,11 @@ namespace Barotrauma public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { - 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 ?? GUIStyle.LargeFont)); + lock (mutex) + { + 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 ?? GUIStyle.LargeFont)); + } if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2610,34 +2601,37 @@ namespace Barotrauma var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } - bool overlapFound = true; - int tries = 0; - while (overlapFound) - { - overlapFound = false; - foreach (var otherMessage in messages) - { - float xDiff = otherMessage.Pos.X - newMessage.Pos.X; - if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } - float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; - if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } - Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); - if (moveDir.LengthSquared() > 0.0001f) - { - moveDir = Vector2.Normalize(moveDir); - } - else - { - moveDir = Rand.Vector(1.0f); - } - moveDir.Y = -Math.Abs(moveDir.Y); - newMessage.Pos -= Vector2.UnitY * 10; - } - tries++; - if (tries > 20) { break; } - } - messages.Add(newMessage); + lock (mutex) + { + bool overlapFound = true; + int tries = 0; + while (overlapFound) + { + overlapFound = false; + foreach (var otherMessage in messages) + { + float xDiff = otherMessage.Pos.X - newMessage.Pos.X; + if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; } + float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y; + if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; } + Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f)); + if (moveDir.LengthSquared() > 0.0001f) + { + moveDir = Vector2.Normalize(moveDir); + } + else + { + moveDir = Rand.Vector(1.0f); + } + moveDir.Y = -Math.Abs(moveDir.Y); + newMessage.Pos -= Vector2.UnitY * 10; + } + tries++; + if (tries > 20) { break; } + } + messages.Add(newMessage); + } } public static void ClearMessages() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index d27e2c086..10334efc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,9 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - private static readonly object mutex = new object(); - - protected GUICanvas() : base(size, parent: null) { } + protected GUICanvas() : base(Size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -33,7 +31,7 @@ namespace Barotrauma //GUICanvas stores the children as weak references, to allow elements that we no longer need to get garbage collected private readonly List> childrenWeakRef = new List>(); - private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + private static Vector2 Size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); protected override Rectangle NonScaledUIRect => UIRect; @@ -41,25 +39,27 @@ namespace Barotrauma private static void OnChildrenChanged(RectTransform _) { - lock (mutex) + CrossThread.RequestExecutionOnMainThread(RefreshChildren); + } + + private static void RefreshChildren() + { + //add weak reference if we don't have one yet + foreach (var child in _instance.Children) { - //add weak reference if we don't have one yet - foreach (var child in _instance.Children) + if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) { - if (!_instance.childrenWeakRef.Any(c => c.TryGetTarget(out var existingChild) && existingChild == child)) - { - _instance.childrenWeakRef.Add(new WeakReference(child)); - } + _instance.childrenWeakRef.Add(new WeakReference(child)); } - //get rid of strong references - _instance.children.Clear(); - //remove dead children - for (int i = _instance.childrenWeakRef.Count - 2; i >= 0; i--) + } + //get rid of strong references + _instance.children.Clear(); + //remove dead children + for (int i = _instance.childrenWeakRef.Count - 1; i >= 0; i--) + { + if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) { - if (!_instance.childrenWeakRef[i].TryGetTarget(out var child) || child.Parent != _instance) - { - _instance.childrenWeakRef.RemoveAt(i); - } + _instance.childrenWeakRef.RemoveAt(i); } } } @@ -67,7 +67,7 @@ namespace Barotrauma // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Vector2 recalculatedSize = size; + Vector2 recalculatedSize = Size; // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well for (int i = 0; i < Instance.childrenWeakRef.Count; i++) @@ -109,7 +109,7 @@ namespace Barotrauma } } - Instance.Resize(size, resizeChildren: true); + Instance.Resize(Size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); _instance.children.Clear(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index dd848bd65..8a795ccd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -244,18 +244,16 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) + public GUIComponent AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null, Color? color = null, Color? textColor = null) { toolTip ??= ""; if (selectMultiple) { - var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, style: "ListBoxElement") + var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color) { UserData = userData, ToolTip = toolTip }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text) { UserData = userData, @@ -275,7 +273,7 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (tickBox.Selected) + if (tickBox is { Selected: true }) { selectedDataMultiple.Add(child.UserData); selectedIndexMultiple.Add(i); @@ -289,11 +287,11 @@ namespace Barotrauma return true; } }; + return frame; } else { - new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) - { IsFixedSize = false }, text, style: "ListBoxElement") + return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor) { UserData = userData, ToolTip = toolTip @@ -323,7 +321,7 @@ namespace Barotrauma } else { - if (!(component is GUITextBlock textBlock)) + if (component is not GUITextBlock textBlock) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index b03bc27a6..aa1a6a882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1059,6 +1059,7 @@ namespace Barotrauma GUIComponent child = Content.GetChild(childIndex); if (child is null) { return; } + if (!child.Enabled) { return; } bool wasSelected = true; if (OnSelected != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index fc1ade91e..4d3f69ea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -268,7 +268,7 @@ namespace Barotrauma Buttons.Clear(); } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, textColor: GUIStyle.TextColorBright); GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index 4f938a89f..9edfae736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -313,7 +313,9 @@ namespace Barotrauma break; } - RectTransform.MinSize = TextBox.RectTransform.MinSize; + RectTransform.MinSize = new Point( + Math.Max(rectT.MinSize.X, TextBox.RectTransform.MinSize.X), + Math.Max(rectT.MinSize.Y, TextBox.RectTransform.MinSize.Y)); LayoutGroup.Recalculate(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 05d2898e5..ea2b67b4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -448,7 +448,7 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + if ((PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked()) && selected) { if (!mouseHeldInside) { Deselect(); } mouseHeldInside = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index dec3485bb..98921a51b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -143,14 +143,13 @@ namespace Barotrauma } int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); - HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom + ButtonAreaTop.Height, messageAreaWidth, ButtonAreaTop.Height); - bool isFourByThree = GUI.IsFourByThree(); - int chatBoxWidth = !isFourByThree ? (int)(475 * GUI.Scale) : (int)(375 * GUI.Scale); + int chatBoxWidth = (int)(475 * GUI.Scale * GUI.AspectRatioAdjustment); int chatBoxHeight = (int)Math.Max(GameMain.GraphicsHeight * 0.25f, 150); ChatBoxArea = new Rectangle(Padding, GameMain.GraphicsHeight - Padding - chatBoxHeight, chatBoxWidth, chatBoxHeight); @@ -187,19 +186,26 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { - DrawRectangle(ButtonAreaTop, Color.White * 0.5f); - DrawRectangle(TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); - DrawRectangle(MessageAreaTop, GUIStyle.Orange * 0.5f); - DrawRectangle(CrewArea, Color.Blue * 0.5f); - DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); - DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f); - DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f); - DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f); - DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f); - DrawRectangle(ItemHUDArea, Color.Magenta * 0.3f); + DrawRectangle(nameof(ButtonAreaTop), ButtonAreaTop, Color.White * 0.5f); + DrawRectangle(nameof(TutorialObjectiveListArea), TutorialObjectiveListArea, GUIStyle.Blue * 0.5f); + DrawRectangle(nameof(MessageAreaTop), MessageAreaTop, GUIStyle.Orange * 0.5f); + DrawRectangle(nameof(CrewArea), CrewArea, Color.Blue * 0.5f); + DrawRectangle(nameof(ChatBoxArea), ChatBoxArea, Color.Cyan * 0.5f); + DrawRectangle(nameof(HealthBarArea), HealthBarArea, Color.Red * 0.5f); + DrawRectangle(nameof(HealthBarAfflictionArea), HealthBarAfflictionArea, Color.Red * 0.5f); + DrawRectangle(nameof(InventoryAreaLower), InventoryAreaLower, Color.Yellow * 0.5f); + DrawRectangle(nameof(HealthWindowAreaLeft), HealthWindowAreaLeft, Color.Red * 0.5f); + DrawRectangle(nameof(BottomRightInfoArea), BottomRightInfoArea, Color.Green * 0.5f); + DrawRectangle(nameof(ItemHUDArea), ItemHUDArea, Color.Magenta * 0.3f); - void DrawRectangle(Rectangle r, Color c) => GUI.DrawRectangle(spriteBatch, r, c); + void DrawRectangle(string label, Rectangle r, Color c) + { + if (!label.IsNullOrEmpty()) + { + GUI.DrawString(spriteBatch, r.Location.ToVector2() + Vector2.One * 3, label, c, font: GUIStyle.SmallFont); + } + GUI.DrawRectangle(spriteBatch, r, c); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index b8026a67c..0c5c6cc17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -11,9 +11,9 @@ namespace Barotrauma { class LoadingScreen { - private readonly Texture2D defaultBackgroundTexture, overlay; + private readonly Sprite defaultBackgroundTexture, overlay; private readonly SpriteSheet decorativeGraph, decorativeMap; - private Texture2D currentBackgroundTexture; + private Sprite currentBackgroundTexture; private readonly Sprite noiseSprite; private string randText = ""; @@ -24,6 +24,8 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; + private bool mirrorBackground; + public struct PendingSplashScreen { public string Filename; @@ -112,12 +114,12 @@ namespace Barotrauma public LoadingScreen(GraphicsDevice graphics) { - defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); + defaultBackgroundTexture = new Sprite("Content/Map/LocationPortraits/MainMenu1.png", Vector2.Zero); decorativeMap = new SpriteSheet("Content/Map/MapHUD.png", 6, 5, Vector2.Zero, sourceRect: new Rectangle(0, 0, 2048, 640)); decorativeGraph = new SpriteSheet("Content/Map/MapHUD.png", 4, 10, Vector2.Zero, sourceRect: new Rectangle(1025, 1259, 1024, 732)); - overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); + overlay = new Sprite("Content/UI/MainMenuVignette.png", Vector2.Zero); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; SetSelectedTip(TextManager.Get("LoadingScreenTip")); @@ -138,35 +140,24 @@ namespace Barotrauma DisableSplashScreen(); } } - - var titleStyle = GUIStyle.GetComponentStyle("TitleText"); - Sprite titleSprite = null; - if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) - { - titleSprite = titleStyle.Sprites[GUIComponent.ComponentState.None].First()?.Sprite; - } drawn = true; currentBackgroundTexture ??= defaultBackgroundTexture; + float overlayScale = Math.Min(GameMain.GraphicsWidth / overlay.size.X, GameMain.GraphicsHeight / overlay.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(overlay.size.X * overlayScale / 2), 0, + (int)(GameMain.GraphicsWidth - overlay.size.X * overlayScale / 2), GameMain.GraphicsHeight); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState); - float scale = (GameMain.GraphicsWidth / (float)currentBackgroundTexture.Width) * 1.2f; - float paddingX = currentBackgroundTexture.Width * scale - GameMain.GraphicsWidth; - float paddingY = currentBackgroundTexture.Height * scale - GameMain.GraphicsHeight; - - double noiseT = (Timing.TotalTime * 0.02f); - Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); - pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); - - spriteBatch.Draw(currentBackgroundTexture, - new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, new Vector2(currentBackgroundTexture.Width / 2, currentBackgroundTexture.Height / 2), - scale, SpriteEffects.None, 0.0f); - - spriteBatch.Draw(overlay, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), null, Color.White, 0.0f, Vector2.Zero, SpriteEffects.None, 0.0f); + GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea, + spriteEffects: mirrorBackground ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale); + double noiseT = Timing.TotalTime * 0.02f; float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; noiseSprite.DrawTiled(spriteBatch, Vector2.Zero, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight), @@ -174,10 +165,7 @@ namespace Barotrauma color: Color.White * noiseStrength * 0.1f, textureScale: Vector2.One * noiseScale); - titleSprite?.Draw(spriteBatch, new Vector2(GameMain.GraphicsWidth * 0.05f, GameMain.GraphicsHeight * 0.125f), - Color.White, origin: new Vector2(0.0f, titleSprite.SourceRect.Height / 2.0f), - scale: GameMain.GraphicsHeight / 2000.0f); - + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.75f)); if (WaitForLanguageSelection) { DrawLanguageSelectionPrompt(spriteBatch, graphics); @@ -215,16 +203,18 @@ namespace Barotrauma #endif } } + if (GUIStyle.LargeFont.HasValue) { GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), - new Vector2(GameMain.GraphicsWidth / 2.0f - GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), + textPos, Color.White); + textPos.Y += GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).Y * 1.2f; } if (GUIStyle.Font.HasValue && selectedTip != null) { - string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.5f, GUIStyle.Font.Value); + string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y; @@ -234,7 +224,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } @@ -244,7 +235,8 @@ namespace Barotrauma for (int i = 0; i < lines.Length; i++) { GUIStyle.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)), + new Color(228, 217, 167, 255)); } } } @@ -257,13 +249,16 @@ namespace Barotrauma Vector2 decorativeScale = new Vector2(GameMain.GraphicsHeight / 1080.0f); float noiseVal = (float)PerlinNoise.CalculatePerlin(Timing.TotalTime * 0.25f, Timing.TotalTime * 0.5f, 0); - decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.001f, GameMain.GraphicsHeight * 0.24f), - Color.White, Vector2.Zero, 0.0f, decorativeScale, SpriteEffects.FlipVertically); + if (!WaitForLanguageSelection) + { + decorativeGraph.Draw(spriteBatch, (int)(decorativeGraph.FrameCount * noiseVal), + new Vector2(GameMain.GraphicsWidth * 0.001f, textPos.Y), + Color.White, new Vector2(0, decorativeMap.FrameSize.Y), 0.0f, decorativeScale, SpriteEffects.FlipVertically); + } decorativeMap.Draw(spriteBatch, (int)(decorativeMap.FrameCount * noiseVal), - new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.66f), - Color.White, decorativeMap.FrameSize.ToVector2(), 0.0f, decorativeScale); + new Vector2(GameMain.GraphicsWidth * 0.99f, GameMain.GraphicsHeight * 0.01f), + Color.White, new Vector2(decorativeMap.FrameSize.X, 0), 0.0f, decorativeScale, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically); if (noiseVal < 0.2f) { @@ -285,8 +280,9 @@ namespace Barotrauma if (GUIStyle.LargeFont.HasValue) { + Vector2 textSize = GUIStyle.LargeFont.MeasureString(randText); GUIStyle.LargeFont.DrawString(spriteBatch, randText, - new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), + new Vector2(GameMain.GraphicsWidth * 0.95f - textSize.X, GameMain.GraphicsHeight * 0.06f), Color.White * (1.0f - noiseVal)); } @@ -312,8 +308,8 @@ namespace Barotrauma languageSelectionCursor = new Sprite("Content/UI/cursor.png", Vector2.Zero); } - Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); - Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / AvailableLanguages.Length); + Vector2 textPos = new Vector2((int)(GameMain.GraphicsWidth * 0.05f), (int)(GameMain.GraphicsHeight * 0.3f)); + Vector2 textSpacing = new Vector2(0.0f, GameMain.GraphicsHeight * 0.5f / AvailableLanguages.Length); foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); @@ -321,10 +317,10 @@ namespace Barotrauma Vector2 textSize = font.MeasureString(localizedLanguageName); bool hover = - Math.Abs(PlayerInput.MousePosition.X - textPos.X) < textSize.X / 2 && - Math.Abs(PlayerInput.MousePosition.Y - textPos.Y) < textSpacing.Y / 2; + PlayerInput.MousePosition.X > textPos.X && PlayerInput.MousePosition.X < textPos.X + textSize.X && + PlayerInput.MousePosition.Y > textPos.Y && PlayerInput.MousePosition.Y < textPos.Y + textSize.Y; - font.DrawString(spriteBatch, localizedLanguageName, textPos - textSize / 2, + font.DrawString(spriteBatch, localizedLanguageName, textPos, hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { @@ -431,7 +427,12 @@ namespace Barotrauma drawn = false; LoadState = null; SetSelectedTip(TextManager.Get("LoadingScreenTip")); - currentBackgroundTexture = LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + currentBackgroundTexture = LocationType.Prefabs.Where(p => p.UsePortraitInRandomLoadingScreens).GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue)); + if (GameMain.GameSession?.GameMode?.Missions is { } missions && missions.Any(m => m.Prefab.HasPortraits)) + { + currentBackgroundTexture = missions.Where(m => m.Prefab.HasPortraits).First().Prefab.GetPortrait(Rand.Int(int.MaxValue)); + } + mirrorBackground = Rand.Range(0.0f, 1.0f) < 0.5f; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 6f5272743..81598fdf2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -143,33 +143,32 @@ namespace Barotrauma { public readonly MedicalClinic.NetAffliction Target; public readonly ImmutableArray ElementsToDisable; + public readonly GUIComponent TargetElement; - public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + public PopupAffliction(ImmutableArray elementsToDisable, GUIComponent component, MedicalClinic.NetAffliction target) { Target = target; ElementsToDisable = elementsToDisable; + TargetElement = component; } } private readonly struct PopupAfflictionList { public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIListBox ListElement; public readonly GUIButton TreatAllButton; - public readonly List Afflictions; + public readonly HashSet Afflictions; - public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIListBox listElement, GUIButton treatAllButton) { + ListElement = listElement; Target = crewMember; TreatAllButton = treatAllButton; - Afflictions = new List(); + Afflictions = new HashSet(); } } - // private enum SortMode - // { - // Severity - // } - private readonly MedicalClinic medicalClinic; private readonly GUIComponent container; private Point prevResolution; @@ -221,23 +220,22 @@ namespace Barotrauma private void UpdatePopupAfflictions() { - if (selectedCrewAfflictionList is { } afflictionList) - { - foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) - { - ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); - if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) - { - ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); - } - } + if (selectedCrewAfflictionList is not { } afflictionList) { return; } - afflictionList.TreatAllButton.Enabled = true; - if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) { - afflictionList.TreatAllButton.Enabled = false; + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); } } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } } private void UpdatePending() @@ -309,7 +307,7 @@ namespace Barotrauma } } - private void UpdateCrewPanel() + public void UpdateCrewPanel() { if (crewHealList is not { } healList) { return; } @@ -502,7 +500,7 @@ namespace Barotrauma return true; } }; - + crewHealList = new CrewHealList(crewList, parent, treatAllButton); void OnReceived(MedicalClinic.CallbackOnlyRequest obj) @@ -789,7 +787,7 @@ namespace Barotrauma GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; - PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, afflictionList, treatAllButton); selectedCrewElement = mainFrame; selectedCrewAfflictionList = popupAfflictionList; @@ -810,9 +808,9 @@ namespace Barotrauma List allComponents = new List(); foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) { - ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); - allComponents.AddRange(createdComponents); - popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents.AllCreatedElements); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, affliction)); } allComponents.Add(treatAllButton); @@ -832,9 +830,11 @@ namespace Barotrauma } } - private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + private readonly record struct CreatedPopupAfflictionElement(GUIComponent MainElement, ImmutableArray AllCreatedElements); + + private CreatedPopupAfflictionElement CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) { - if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + ToolBox.ThrowIfNull(affliction.Prefab); GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), backgroundFrame.RectTransform, Anchor.BottomCenter), style: "HorizontalLine"); @@ -846,9 +846,9 @@ namespace Barotrauma GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; - Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + Color iconColor = CharacterHealth.GetAfflictionIconColor(affliction.Prefab, affliction.Strength); - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), affliction.Prefab.Icon, scaleToFit: true) { Color = iconColor, DisabledColor = iconColor * 0.5f @@ -856,7 +856,7 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), affliction.Prefab.Name, font: GUIStyle.SubHeadingFont); Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -878,7 +878,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(affliction.Prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -923,7 +923,7 @@ namespace Barotrauma return true; }; - return elementsToDisable; + return new CreatedPopupAfflictionElement(backgroundFrame, elementsToDisable); } private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) @@ -1033,11 +1033,53 @@ namespace Barotrauma } } + public void UpdateAfflictions(MedicalClinic.NetCrewMember crewMember) + { + if (selectedCrewAfflictionList is not { } afflictionList || !afflictionList.Target.CharacterEquals(crewMember)) { return; } + + List allComponents = new List(); + foreach (PopupAffliction existingAffliction in afflictionList.Afflictions.ToHashSet()) + { + if (crewMember.Afflictions.None(received => received.AfflictionEquals(existingAffliction.Target))) + { + // remove from UI + existingAffliction.TargetElement.RectTransform.Parent = null; + afflictionList.Afflictions.Remove(existingAffliction); + } + else + { + allComponents.AddRange(existingAffliction.ElementsToDisable); + } + } + + foreach (MedicalClinic.NetAffliction received in crewMember.Afflictions) + { + // we're not that concerned about updating the strength of the afflictions + if (afflictionList.Afflictions.Any(existing => existing.Target.AfflictionEquals(received))) { continue; } + + CreatedPopupAfflictionElement createdComponents = CreatePopupAffliction(afflictionList.ListElement.Content, crewMember, received); + allComponents.AddRange(createdComponents.AllCreatedElements); + afflictionList.Afflictions.Add(new PopupAffliction(createdComponents.AllCreatedElements, createdComponents.MainElement, received)); + } + + allComponents.Add(afflictionList.TreatAllButton); + afflictionList.TreatAllButton.OnClicked = (_, _) => + { + var afflictions = crewMember.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + UpdatePopupAfflictions(); + } + public void ClosePopup() { if (selectedCrewElement is { } popup) { - popup.Parent?.RemoveChild(selectedCrewElement); + popup.RectTransform.Parent = null; } selectedCrewElement = null; @@ -1096,5 +1138,14 @@ namespace Barotrauma refreshTimer = 0; } } + + public void OnDeselected() + { + if (GameMain.NetworkMember is not null) + { + MedicalClinic.SendUnsubscribeRequest(); + } + ClosePopup(); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 7eb393015..1cb5e37ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -207,11 +207,12 @@ namespace Barotrauma cargoManager.OnItemsInSellFromSubCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingFromSubRefresh = true); } - public void SelectStore(Identifier identifier) + public void SelectStore(Character merchant) { + Identifier storeIdentifier = merchant?.MerchantIdentifier ?? Identifier.Empty; if (CurrentLocation?.Stores != null) { - if (!identifier.IsEmpty && CurrentLocation.GetStore(identifier) is { } store) + if (!storeIdentifier.IsEmpty && CurrentLocation.GetStore(storeIdentifier) is { } store) { ActiveStore = store; if (storeNameBlock != null) @@ -223,12 +224,13 @@ namespace Barotrauma } storeNameBlock.SetRichText(storeName); } + ActiveStore.SetMerchantFaction(merchant.Faction); } else { ActiveStore = null; string errorId, msg; - if (identifier.IsEmpty) + if (storeIdentifier.IsEmpty) { errorId = "Store.SelectStore:IdentifierEmpty"; msg = $"Error selecting store at {CurrentLocation}: identifier is empty."; @@ -236,7 +238,7 @@ namespace Barotrauma else { errorId = "Store.SelectStore:StoreDoesntExist"; - msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; } DebugConsole.LogError(msg); GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg); @@ -249,17 +251,17 @@ namespace Barotrauma if (campaignUI.Campaign.Map == null) { errorId = "Store.SelectStore:MapNull"; - msg = $"Error selecting store with identifier \"{identifier}\": Map is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": Map is null."; } else if (CurrentLocation == null) { errorId = "Store.SelectStore:CurrentLocationNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation is null."; } else if (CurrentLocation.Stores == null) { errorId = "Store.SelectStore:StoresNull"; - msg = $"Error selecting store with identifier \"{identifier}\": CurrentLocation.Stores is null."; + msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation.Stores is null."; } if (!msg.IsNullOrEmpty()) { @@ -406,11 +408,11 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - if (CurrentLocation != null) + if (ActiveStore is not null) { Color textColor = GUIStyle.ColorReputationNeutral; string sign = ""; - int reputationModifier = (int)MathF.Round((CurrentLocation.GetStoreReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); + int reputationModifier = (int)MathF.Round((ActiveStore.GetReputationModifier(activeTab == StoreTab.Buy) - 1) * 100); if (reputationModifier > 0) { textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh; @@ -727,7 +729,7 @@ namespace Barotrauma ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - CurrentLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); + newLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); } } @@ -855,6 +857,28 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } + private static KeyValuePair? GetReputationRequirement(PriceInfo priceInfo) + { + return GameMain.GameSession?.Campaign is not null + ? priceInfo.MinReputation.FirstOrNull() + : null; + } + + private static KeyValuePair? GetTooLowReputation(PriceInfo priceInfo) + { + if (GameMain.GameSession?.Campaign is CampaignMode campaign) + { + foreach (var minRep in priceInfo.MinReputation) + { + if (campaign.GetReputation(minRep.Key) < minRep.Value) + { + return minRep; + } + } + } + return null; + } + int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount; private void RefreshStoreBuyList() @@ -898,6 +922,7 @@ namespace Barotrauma { if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : @@ -922,7 +947,8 @@ namespace Barotrauma SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } - SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); + + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue); existingItemFrames.Add(itemFrame); } } @@ -1317,6 +1343,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = itemX.ItemPrefab.Name != itemY.ItemPrefab.Name ? itemX.ItemPrefab.Name.CompareTo(itemY.ItemPrefab.Name) : itemX.ItemPrefab.Identifier.CompareTo(itemY.ItemPrefab.Identifier); @@ -1345,6 +1373,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1369,6 +1399,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } @@ -1391,10 +1423,12 @@ namespace Barotrauma specialsGroup.Recalculate(); } - static int CompareByCategory(RectTransform x, RectTransform y) + int CompareByCategory(RectTransform x, RectTransform y) { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { + int reputationCompare = CompareByReputationRestriction(itemX, itemY); + if (reputationCompare != 0) { return reputationCompare; } return itemX.ItemPrefab.Category.CompareTo(itemY.ItemPrefab.Category); } else @@ -1424,6 +1458,19 @@ namespace Barotrauma } } + int CompareByReputationRestriction(PurchasedItem item1, PurchasedItem item2) + { + PriceInfo priceInfo1 = item1.ItemPrefab.GetPriceInfo(ActiveStore); + PriceInfo priceInfo2 = item2.ItemPrefab.GetPriceInfo(ActiveStore); + if (priceInfo1 != null && priceInfo2 != null) + { + var requiredReputation1 = GetTooLowReputation(priceInfo1)?.Value ?? 0.0f; + var requiredReputation2 = GetTooLowReputation(priceInfo2)?.Value ?? 0.0f; + return requiredReputation1.CompareTo(requiredReputation2); + } + return 0; + } + static int CompareByElement(RectTransform x, RectTransform y) { if (ShouldBeOnTop(x) || ShouldBeOnBottom(y)) @@ -1753,7 +1800,7 @@ namespace Barotrauma { if (pi.ItemPrefab?.InventoryIcon != null) { - icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); + icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f : 0.5f); } else if (pi.ItemPrefab?.Sprite != null) { @@ -1858,7 +1905,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.GetTooltip(); + toolTip = purchasedItem.ItemPrefab.GetTooltip(Character.Controlled); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1871,6 +1918,23 @@ namespace Barotrauma toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}"; } } + + PriceInfo priceInfo = purchasedItem.ItemPrefab.GetPriceInfo(ActiveStore); + var campaign = GameMain.GameSession?.Campaign; + if (priceInfo != null && campaign != null) + { + var requiredReputation = GetReputationRequirement(priceInfo); + if (requiredReputation != null) + { + var repStr = TextManager.GetWithVariables( + "campaignstore.reputationrequired", + ("[amount]", ((int)requiredReputation.Value.Value).ToString()), + ("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value)); + Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ? + GUIStyle.Orange : GUIStyle.Green; + toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖"; + } + } } itemComponent.ToolTip = RichString.Rich(toolTip); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index ed1740549..d1ea71205 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -15,8 +15,6 @@ namespace Barotrauma private int pageCount; private readonly bool transferService, purchaseService; private bool initialized; - private int deliveryFee; - private string deliveryLocationName; public GUIFrame GuiFrame; private GUIFrame pageIndicatorHolder; @@ -34,14 +32,13 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, selectedSubText, switchText, missingPreviewText, currencyName; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, selectedSubText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; private readonly LocalizedString[] messageBoxOptions; - public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; private static readonly Color indicatorColor = new Color(112, 149, 129); @@ -108,14 +105,9 @@ namespace Barotrauma { initialized = true; selectedSubText = TextManager.Get("selectedsub"); - deliveryText = TextManager.Get("requestdeliverybutton"); switchText = TextManager.Get("switchtosubmarinebutton"); purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); purchaseOnlyText = TextManager.Get("purchase"); - if (transferService) - { - deliveryFee = CalculateDeliveryFee(); - } currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); @@ -124,13 +116,6 @@ namespace Barotrauma CreateGUI(); } - private int CalculateDeliveryFee() - { - int distanceToOutpost = GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - deliveryLocationName = endLocation.Name; - return DeliveryFeePerDistanceTravelled * distanceToOutpost; - } - private void CreateGUI() { createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); @@ -194,7 +179,7 @@ namespace Barotrauma confirmButtonAlt = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseOnlyText, style: "GUIButtonFreeScale"); transferInfoFrameWidth -= confirmButtonAlt.RectTransform.RelativeSize.X; } - confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : deliveryFee > 0 ? deliveryText : switchText, style: "GUIButtonFreeScale"); + confirmButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), bottomContainer.RectTransform), purchaseService ? purchaseAndSwitchText : switchText, style: "GUIButtonFreeScale"); SetConfirmButtonState(false); transferInfoFrameWidth -= confirmButton.RectTransform.RelativeSize.X; GUIFrame transferInfoFrame = new GUIFrame(new RectTransform(new Vector2(transferInfoFrameWidth, 1.0f), bottomContainer.RectTransform), style: null) @@ -406,22 +391,14 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); + LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.GetPrice()); submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("price", "[amount]", amountString); } else { if (subToDisplay.Name != CurrentOrPendingSubmarine().Name) { - if (deliveryFee > 0) - { - LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); - submarineDisplays[i].submarineFee.Text = TextManager.GetWithVariable("deliveryfee", "[amount]", amountString); - } - else - { - submarineDisplays[i].submarineFee.Text = string.Empty; - } + submarineDisplays[i].submarineFee.Text = string.Empty; } else { @@ -581,7 +558,7 @@ namespace Barotrauma if (owned) { - confirmButton.Text = deliveryFee > 0 ? deliveryText : switchText; + confirmButton.Text = switchText; confirmButton.OnClicked = (button, userData) => { ShowTransferPrompt(); @@ -702,37 +679,12 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (!GameMain.GameSession.Campaign.CanAfford(deliveryFee) && deliveryFee > 0) - { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyName), - ("[submarinename]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); - return; - } + var text = TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)); + text += GetItemTransferText(); + GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - GUIMessageBox msgBox; - - if (deliveryFee > 0) - { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", - ("[submarinename1]", selectedSubmarine.DisplayName), - ("[location1]", deliveryLocationName), - ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), - ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyName)), messageBoxOptions); - msgBox.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction; - } - else - { - var text = TextManager.GetWithVariables("switchsubmarinetext", - ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), - ("[submarinename2]", selectedSubmarine.DisplayName)); - text += GetItemTransferText(); - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), text, messageBoxOptions); - } msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -777,7 +729,7 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, deliveryFee); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -797,7 +749,9 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (!GameMain.GameSession.Campaign.CanAfford(selectedSubmarine.Price)) + int price = selectedSubmarine.GetPrice(); + + if (!GameMain.GameSession.Campaign.CanAfford(price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyName), @@ -810,7 +764,7 @@ namespace Barotrauma { var text = TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)); text += GetItemTransferText(); @@ -854,7 +808,7 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch, 0); + GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); RefreshSubmarineDisplay(true); } else @@ -868,7 +822,7 @@ namespace Barotrauma { msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), - ("[amount]", selectedSubmarine.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", currencyName)) + '\n' + TextManager.Get("submarineswitchinstruction"), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 75219550f..6b8ccfbb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -836,7 +836,7 @@ namespace Barotrauma Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}"); campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { - if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } + if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; } SetWalletText(walletBlock, e.Wallet, icon, largeIcon); }); registeredEvents.Add(eventIdentifier); @@ -1502,27 +1502,9 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(10) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); - - var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); - var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)Level.Loaded.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") - { - CanBeFocused = false - }; - - int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; - Sprite portrait = location.Type.GetPortrait(location.PortraitId); bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; int contentWidth = missionFrameContent.Rect.Width; - if (hasPortrait) { float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; @@ -1534,6 +1516,30 @@ namespace Barotrauma portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y); } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); + + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight); + } + var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); + var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)Level.Loaded.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") + { + CanBeFocused = false + }; + + int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2; + + GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); missionList.ContentBackground.Color = Color.Transparent; missionList.Spacing = GUI.IntScale(15); @@ -1545,6 +1551,7 @@ namespace Barotrauma foreach (Mission mission in GameMain.GameSession.Missions) { + if (!mission.Prefab.ShowInMenus) { continue; } GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) { @@ -1556,7 +1563,7 @@ namespace Barotrauma descriptionText += "\n\n" + missionMessage; } RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); - RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString reputationText = mission.GetReputationRewardText(); Func wrapMissionText(GUIFont font) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs index 9b5318e30..6f440255e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -727,7 +727,7 @@ namespace Barotrauma talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); } - TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked) + TalentStages collectiveStage = talentStages.All(static stage => stage is Locked) ? Locked : Available; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3a2e531c1..3bdf2d65d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Barotrauma.Extensions; @@ -77,6 +78,8 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; + private static ImmutableHashSet characterList = ImmutableHashSet.Empty; + /// /// While set to true any call to will cause the buy button to be disabled and to not update the prices. /// This is to prevent us from buying another upgrade before the server has given us the new prices and causing potential syncing issues. @@ -102,6 +105,7 @@ namespace Barotrauma public UpgradeStore(CampaignUI campaignUI, GUIComponent parent) { WaitForServerUpdate = false; + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); this.campaignUI = campaignUI; GUIFrame upgradeFrame = new GUIFrame(rectT(1, 1, parent, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { @@ -130,6 +134,7 @@ namespace Barotrauma private void RefreshAll() { + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); switch (selectedUpgradeTab) { case UpgradeTab.Repairs: @@ -273,7 +278,7 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); - GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, GUI.IsFourByThree() ? 0.98f : 0.95f, parent, Anchor.Center), style: null); + GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, 0.95f, parent, Anchor.Center), style: null); mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f }; topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true); @@ -295,8 +300,8 @@ namespace Barotrauma new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; - GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; - GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; + GUIButton repairButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; /* RIGHT HEADER LAYOUT * |---------------------------------------------------------------------------------------------------| @@ -347,12 +352,15 @@ namespace Barotrauma SelectTab(UpgradeTab.Upgrade); - var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.27f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(GUI.IsFourByThree() ? 0.5f : 0.47f, 0.0f) }, DrawItemSwapPreview) + var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.25f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) + { RelativeOffset = new Vector2(0.52f * GUI.AspectRatioAdjustment, 0.0f) }, DrawItemSwapPreview) { IgnoreLayoutGroups = true, CanBeFocused = true }; + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, repairButton.TextBlock); + #if DEBUG // creates a button that re-creates the UI CreateRefreshButton(); @@ -725,7 +733,7 @@ namespace Barotrauma if (storeLayout == null || mainStoreLayout == null) { return; } currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); - selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; + selectedUpgradeCategoryLayout = new GUIFrame(rectT(0.3f * GUI.AspectRatioAdjustment, 1, mainStoreLayout), style: null) { CanBeFocused = false }; RefreshUpgradeList(); @@ -956,7 +964,7 @@ namespace Barotrauma bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), currentOrPending.UpgradePreviewSprite, item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity), currentOrPending.Description, 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame); @@ -996,7 +1004,7 @@ namespace Barotrauma int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count(); - frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, price, replacement, addBuyButton: true, addProgressBar: false, @@ -1102,7 +1110,7 @@ namespace Barotrauma public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } @@ -1129,7 +1137,8 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.35f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + //name.RectTransform.MinSize = new Point(0, (int)name.TextSize.Y); GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; @@ -1171,7 +1180,7 @@ namespace Barotrauma materialCostList.Visible = false; materialCostList.UserData = UpgradeStoreUserData.MaterialCostList; - var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Right) + var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.CenterRight) { UserData = UpgradeStoreUserData.PriceLabel, //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades @@ -1258,7 +1267,7 @@ namespace Barotrauma { LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), - ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); + ("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1673,7 +1682,7 @@ namespace Barotrauma GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true); priceLabel.Visible = true; - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList); if (!WaitForServerUpdate) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 6afb4c50f..3a217bf1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -163,6 +163,7 @@ namespace Barotrauma private void SetSubmarineVotingText(Client starter, SubmarineInfo info, bool transferItems, VoteType type) { + int price = info.GetPrice(); string name = starter.Name; JobPrefab prefab = starter?.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; @@ -177,35 +178,21 @@ namespace Barotrauma text = TextManager.GetWithVariables(tag, ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: text = TextManager.GetWithVariables("submarinepurchasevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), - ("[amount]", info.Price.ToString()), + ("[amount]", price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - if (deliveryFee > 0) - { - tag = transferItems ? "submarineswitchwithitemsfeevote" : "submarineswitchfeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString), - ("[locationname]", endLocation.Name), - ("[amount]", deliveryFee.ToString()), - ("[currencyname]", TextManager.Get("credit").ToLower())); - } - else - { - tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; - text = TextManager.GetWithVariables(tag, - ("[playername]", characterRichString), - ("[submarinename]", submarineRichString)); - } + tag = transferItems ? "submarineswitchwithitemsnofeevote" : "submarineswitchnofeevote"; + text = TextManager.GetWithVariables(tag, + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); break; } votingOnText = RichString.Rich(text); @@ -218,6 +205,7 @@ namespace Barotrauma private LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, int yesVoteCount, int noVoteCount, bool votePassed) { + int price = info.GetPrice(); LocalizedString result = string.Empty; switch (type) @@ -225,7 +213,7 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]" , noVoteCount.ToString())); @@ -233,31 +221,16 @@ namespace Barotrauma case VoteType.PurchaseSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", price)), ("[currencyname]", TextManager.Get("credit").ToLower()), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]", noVoteCount.ToString())); break; case VoteType.SwitchSub: - int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - - if (deliveryFee > 0) - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", - ("[submarinename]", info.DisplayName), - ("[locationname]", endLocation.Name), - ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", deliveryFee)), - ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } - else - { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", - ("[submarinename]", info.DisplayName), - ("[yesvotecount]", yesVoteCount.ToString()), - ("[novotecount]", noVoteCount.ToString())); - } + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); break; default: break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 135b192d1..ce8c8f655 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -235,9 +235,8 @@ namespace Barotrauma LuaCs = new LuaCsSetup(); GameSettings.Init(); + CreatureMetrics.Init(); - Md5Hash.Cache.Load(); - ConsoleArguments = args; try @@ -755,7 +754,7 @@ namespace Barotrauma } else if (HasLoaded) { - if (ConnectCommand is Some { Value: var connectCommand }) + if (ConnectCommand.TryUnwrap(out var connectCommand)) { if (Client != null) { @@ -1081,6 +1080,7 @@ namespace Barotrauma public static void QuitToMainMenu(bool save) { + CreatureMetrics.Save(); if (save) { GUI.SetSavingIndicatorState(true); @@ -1188,6 +1188,7 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { exiting = true; + CreatureMetrics.Save(); DebugConsole.NewMessage("Exiting..."); Client?.Quit(); SteamManager.ShutDown(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 7a09abd5e..377bba240 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -194,7 +193,7 @@ namespace Barotrauma }; } - var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -1403,8 +1402,7 @@ namespace Barotrauma bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && - (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton == MouseButton.PrimaryMouse; bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -2796,8 +2794,8 @@ namespace Barotrauma var orderName = GetOrderNameBasedOnContextuality(order); var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, tooltip: !showAssignmentTooltip ? orderName : orderName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); if (disableNode) { @@ -2999,8 +2997,8 @@ namespace Barotrauma var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + - "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); + "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") + + "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip")); } if (!CanCharacterBeHeard()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs index 8e0430760..3e60238ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using System.Collections.Immutable; using System.Linq; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { @@ -11,21 +9,22 @@ namespace Barotrauma { private const int MaxDrawnElements = 12; - public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, int debugDrawMetadataOffset, string[] ignoredMetadataInfo) + public void DebugDraw(SpriteBatch spriteBatch, Vector2 pos, CampaignMode campaign, GUI.DebugDrawMetaData debugDrawMetaData) { var campaignData = data; - foreach (string ignored in ignoredMetadataInfo) + if (!debugDrawMetaData.FactionMetadata) { removeData("reputation.faction"); } + if (!debugDrawMetaData.UpgradeLevels) { removeData("upgrade."); } + if (!debugDrawMetaData.UpgradePrices) { removeData("upgradeprice."); } + + void removeData(string keyStartsWith) { - if (!string.IsNullOrWhiteSpace(ignored)) - { - campaignData = campaignData.Where(pair => !pair.Key.StartsWith(ignored)).ToDictionary(i => i.Key, i => i.Value); - } + campaignData = campaignData.Where(pair => !pair.Key.StartsWith(keyStartsWith)).ToDictionary(i => i.Key, i => i.Value); } int offset = 0;; if (campaignData.Count > 0) { - offset = debugDrawMetadataOffset % campaignData.Count; + offset = debugDrawMetaData.Offset % campaignData.Count; if (offset < 0) { offset += campaignData.Count; } } @@ -72,7 +71,7 @@ namespace Barotrauma } float y = infoRect.Bottom + 16; - if (Campaign.Factions != null) + if (campaign.Factions != null) { const string factionHeader = "Reputations"; Vector2 factionHeaderSize = GUIStyle.SubHeadingFont.MeasureString(factionHeader); @@ -81,7 +80,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUIStyle.SubHeadingFont); y += factionHeaderSize.Y + 8; - foreach (Faction faction in Campaign.Factions) + foreach (Faction faction in campaign.Factions) { LocalizedString name = faction.Prefab.Name; Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); @@ -94,20 +93,6 @@ namespace Barotrauma y += 15; } } - - Location location = Campaign.Map?.CurrentLocation; - if (location?.Reputation != null) - { - string name = Campaign.Map?.CurrentLocation.Name; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); - y += nameSize.Y + 5; - - float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); - Color color = ToolBox.GradientLerp(normalizedReputation, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); - } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs index 4a91c9026..d004cd9bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial void SettingsChanged(Option balanceChanged, Option rewardChanged) { - if (Owner is Some { Value: var character }) + if (Owner.TryUnwrap(out var character)) { if (!character.IsPlayer) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 3aae45f01..61feab4eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -16,8 +15,6 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected LocalizedString overlayText, overlayTextBottom; - protected Color overlayTextColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; @@ -30,7 +27,13 @@ namespace Barotrauma protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; - public static CancellationTokenSource StartRoundCancellationToken { get; private set; } + public SlideshowPlayer SlideshowPlayer + { + get; + protected set; + } + + private CancellationTokenSource startRoundCancellationToken; public bool ForceMapUI { @@ -59,10 +62,19 @@ namespace Barotrauma { chatBox.ToggleOpen = wasChatBoxOpen; } - if (!value && CampaignUI?.SelectedTab == InteractionType.PurchaseSub) + if (!value) { - SubmarinePreview.Close(); + switch (CampaignUI?.SelectedTab) + { + case InteractionType.PurchaseSub: + SubmarinePreview.Close(); + break; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic?.OnDeselected(); + break; + } } + showCampaignUI = value; } } @@ -77,6 +89,7 @@ namespace Barotrauma { foreach (Mission mission in Missions.ToList()) { + if (!mission.Prefab.ShowStartMessage) { continue; } new GUIMessageBox( RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) @@ -123,32 +136,10 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } - if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) - { - var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); - Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; - LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); - Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); - Vector2 textPos = centerPos - textSize / 2; - backgroundSprite.Draw(spriteBatch, - centerPos, - Color.White * (overlayTextColor.A / 255.0f), - origin: backgroundSprite.size / 2, - rotate: 0.0f, - scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 1.5f)); - - GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); - - if (!overlayTextBottom.IsNullOrEmpty()) - { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; - GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); - } - } } + SlideshowPlayer?.DrawManually(spriteBatch); + if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) { endRoundButton.Visible = false; @@ -188,6 +179,7 @@ namespace Barotrauma case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && + !Level.Loaded.IsEndBiome && (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); @@ -259,11 +251,11 @@ namespace Barotrauma GUI.ClearCursorWait(); - StartRoundCancellationToken = new CancellationTokenSource(); + startRoundCancellationToken = new CancellationTokenSource(); var loadTask = Task.Run(async () => { await Task.Yield(); - Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; + Rand.ThreadId = Environment.CurrentManagedThreadId; try { GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); @@ -273,7 +265,8 @@ namespace Barotrauma roundSummaryScreen.LoadException = e; } Rand.ThreadId = 0; - }, StartRoundCancellationToken.Token); + startRoundCancellationToken = null; + }, startRoundCancellationToken.Token); TaskPool.Add("AsyncCampaignStartRound", loadTask, (t) => { overlayColor = Color.Transparent; @@ -283,6 +276,21 @@ namespace Barotrauma return loadTask; } + public void CancelStartRound() + { + startRoundCancellationToken?.Cancel(); + } + + public void ThrowIfStartRoundCancellationRequested() + { + if (startRoundCancellationToken != null && + startRoundCancellationToken.Token.IsCancellationRequested) + { + startRoundCancellationToken.Token.ThrowIfCancellationRequested(); + startRoundCancellationToken = null; + } + } + protected SubmarineInfo GetPredefinedStartOutpost() { if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) @@ -316,7 +324,7 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); + CampaignUI.SelectTab(npc.CampaignInteractionType, npc); CampaignUI.UpgradeStore?.RequestRefresh(); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 470e9be68..53cf2e86b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -216,51 +216,35 @@ namespace Barotrauma } Character prevControlled = Character.Controlled; - if (prevControlled?.AIController != null) - { - prevControlled.AIController.Enabled = false; - } GUI.DisableHUD = true; if (IsFirstRound) { - Character.Controlled = null; + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) + { + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } + Character.Controlled = null; prevControlled?.ClearInputs(); overlayColor = Color.LightGray; overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables("campaignstart", - ("xxxx", Map.CurrentLocation.Name), ("yyyy", TextManager.Get($"submarineclass.{Submarine.MainSub.Info.SubmarineClass}"))); - float fadeInDuration = 1.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (timer < textDuration) - { - if (GameMain.GameSession == null || Screen.Selected != GameMain.GameScreen) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - // Try to grab the controlled here to prevent inputs, assigned late on multiplayer - if (Character.Controlled != null) - { - prevControlled = Character.Controlled; - Character.Controlled = null; - prevControlled.ClearInputs(); - } - GameMain.GameScreen.Cam.Freeze = true; - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; - } + var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); borders.Location += outpost.WorldPosition.ToPoint(); GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; + prevControlled ??= Character.Controlled; + GameMain.LightManager.LosAlpha = 0.0f; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -272,16 +256,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } overlayColor = Color.Transparent; while (transition.Running) { @@ -409,6 +383,8 @@ namespace Barotrauma base.Update(deltaTime); + SlideshowPlayer?.UpdateManually(deltaTime); + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { @@ -442,7 +418,8 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } - else + //end biome is handled by the server (automatic transition without a map screen when the end of the level is reached) + else if (!Level.Loaded.IsEndBiome) { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost @@ -467,11 +444,17 @@ namespace Barotrauma } } + public override void UpdateWhilePaused(float deltaTime) + { + SlideshowPlayer?.UpdateManually(deltaTime); + } + public override void End(TransitionType transitionType = TransitionType.None) { base.End(transitionType); ForceMapUI = ShowCampaignUI = false; - + SlideshowPlayer?.Finish(); + // remove all event dialogue boxes GUIMessageBox.MessageBoxes.ForEachMod(mb => { @@ -501,7 +484,8 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); @@ -510,32 +494,6 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - Character controlled = Character.Controlled; - if (controlled != null) - { - controlled.AIController.Enabled = false; - } - - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); @@ -844,8 +802,6 @@ namespace Barotrauma { DebugConsole.Log("Received campaign update (Reputation)"); UInt16 id = msg.ReadUInt16(); - float? reputation = null; - if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) @@ -854,11 +810,6 @@ namespace Barotrauma } if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) { - if (reputation.HasValue) - { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); - } foreach (var (identifier, rep) in factionReps) { Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); @@ -871,6 +822,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); } } + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } } if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) @@ -1010,25 +962,20 @@ namespace Barotrauma foreach (NetWalletTransaction transaction in update.Transactions) { WalletInfo info = transaction.Info; - switch (transaction.CharacterID) + if (transaction.CharacterID.TryUnwrap(out var charID)) { - case Some { Value: var charID }: - { - Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); - if (targetCharacter is null) { break; } - Wallet wallet = targetCharacter.Wallet; + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; - wallet.Balance = info.Balance; - wallet.RewardDistribution = info.RewardDistribution; - TryInvokeEvent(wallet, transaction.ChangedData, info); - break; - } - case None _: - { - Bank.Balance = info.Balance; - TryInvokeEvent(Bank, transaction.ChangedData, info); - break; - } + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + } + else + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); } } @@ -1043,7 +990,7 @@ namespace Barotrauma public override bool TryPurchase(Client client, int price) { - if (!AllowedToManageCampaign(ClientPermissions.ManageCampaign)) + if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 722297b66..102c9d921 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -12,7 +12,13 @@ namespace Barotrauma public override bool Paused { - get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map; } + get + { + return + ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || + ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map || + (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown); + } } public override void UpdateWhilePaused(float deltaTime) @@ -31,6 +37,8 @@ namespace Barotrauma } } + SlideshowPlayer?.UpdateManually(deltaTime); + CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } @@ -77,9 +85,9 @@ namespace Barotrauma /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); Settings = settings; + InitFactions(); map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { @@ -89,7 +97,6 @@ namespace Barotrauma CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } - InitCampaignData(); InitUI(); } @@ -99,6 +106,17 @@ namespace Barotrauma private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "metadata": + CampaignMetadata.Load(subElement); + break; + } + } + + InitFactions(); foreach (var subElement in element.Elements()) { @@ -114,9 +132,6 @@ namespace Barotrauma case "map": map = Map.Load(this, subElement); break; - case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); - break; case "cargo": CargoManager.LoadPurchasedItems(subElement); break; @@ -136,11 +151,8 @@ namespace Barotrauma } } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); - InitUI(); //backwards compatibility for saves made prior to the addition of personal wallets @@ -265,7 +277,6 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); @@ -296,34 +307,9 @@ namespace Barotrauma if (IsFirstRound || showCampaignResetText) { - overlayColor = Color.LightGray; - overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); - overlayTextColor = Color.Transparent; - overlayText = TextManager.GetWithVariables(showCampaignResetText ? "campaignend4" : "campaignstart", - ("xxxx", Map.CurrentLocation.Name), - ("yyyy", TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass))); - LocalizedString pressAnyKeyText = TextManager.Get("pressanykey"); - float fadeInDuration = 2.0f; - float textDuration = 10.0f; - float timer = 0.0f; - while (true) + if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) { - if (timer > fadeInDuration) - { - overlayTextBottom = pressAnyKeyText; - if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0 || PlayerInput.PrimaryMouseButtonClicked()) - { - break; - } - } - if (GameMain.GameSession == null) - { - GUI.DisableHUD = false; - yield return CoroutineStatus.Success; - } - overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); - yield return CoroutineStatus.Running; + SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); } var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); @@ -331,7 +317,13 @@ namespace Barotrauma GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); - GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); + while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) + { + GUI.PreventPauseMenuToggle = true; + yield return CoroutineStatus.Running; + } + GUI.PreventPauseMenuToggle = false; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, @@ -343,17 +335,6 @@ namespace Barotrauma AllowInterrupt = true, RemoveControlFromCharacter = false }; - fadeInDuration = 1.0f; - timer = 0.0f; - overlayTextColor = Color.Transparent; - overlayText = ""; - while (timer < fadeInDuration) - { - overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; - yield return CoroutineStatus.Running; - } - overlayColor = Color.Transparent; while (transition.Running) { yield return CoroutineStatus.Running; @@ -440,61 +421,68 @@ namespace Barotrauma TotalPassedLevels++; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; + case TransitionType.End: + EndCampaign(); + IsFirstRound = true; + break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); - - var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, - transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, - fadeOut: false, - panDuration: EndTransitionDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); GUI.ClearMessages(); - Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; - overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); - float fadeOutDuration = endTransition.PanDuration; - float t = 0.0f; - while (t < fadeOutDuration || endTransition.Running) - { - t += CoroutineManager.DeltaTime; - overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); - yield return CoroutineStatus.Running; - } - overlayColor = Color.White; - yield return CoroutineStatus.Running; - //-------------------------------------- - - if (success) + if (transitionType != TransitionType.End) { - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - else - { - PendingSubmarineSwitch = null; - EnableRoundSummaryGameOverState(); - } + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, + fadeOut: false, + panDuration: EndTransitionDuration); - CrewManager?.ClearCurrentOrders(); - - //-------------------------------------- - - SelectSummaryScreen(roundSummary, newLevel, mirror, () => - { - GameMain.GameScreen.Select(); - if (continueButton != null) + Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; + overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); + float fadeOutDuration = endTransition.PanDuration; + float t = 0.0f; + while (t < fadeOutDuration || endTransition.Running) { - continueButton.Visible = true; + t += CoroutineManager.DeltaTime; + overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); + yield return CoroutineStatus.Running; + } + overlayColor = Color.White; + yield return CoroutineStatus.Running; + + //-------------------------------------- + + if (success) + { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + PendingSubmarineSwitch = null; + EnableRoundSummaryGameOverState(); } - GUI.DisableHUD = false; - GUI.ClearCursorWait(); - overlayColor = Color.Transparent; - }); + CrewManager?.ClearCurrentOrders(); + + SelectSummaryScreen(roundSummary, newLevel, mirror, () => + { + GameMain.GameScreen.Select(); + if (continueButton != null) + { + continueButton.Visible = true; + } + + GUI.DisableHUD = false; + GUI.ClearCursorWait(); + overlayColor = Color.Transparent; + }); + } GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; @@ -502,7 +490,10 @@ namespace Barotrauma protected override void EndCampaignProjSpecific() { - CoroutineManager.StartCoroutine(DoEndCampaignCameraTransition(), "DoEndCampaignCameraTransition"); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + GameMain.CampaignEndScreen.Select(); + GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { showCampaignResetText = true; @@ -511,39 +502,14 @@ namespace Barotrauma }; } - private IEnumerable DoEndCampaignCameraTransition() - { - if (Character.Controlled != null) - { - Character.Controlled.AIController.Enabled = false; - Character.Controlled = null; - } - GUI.DisableHUD = true; - ISpatialEntity endObject = Level.Loaded.LevelObjectManager.GetAllObjects().FirstOrDefault(obj => obj.Prefab.SpawnPos == LevelObjectPrefab.SpawnPosType.LevelEnd); - var transition = new CameraTransition(endObject ?? Submarine.MainSub, GameMain.GameScreen.Cam, - null, Alignment.Center, - fadeOut: true, - panDuration: 10, - startZoom: null, endZoom: 0.2f); - - while (transition.Running) - { - yield return CoroutineStatus.Running; - } - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - GameMain.CampaignEndScreen.Select(); - GUI.DisableHUD = false; - - yield return CoroutineStatus.Success; - } - public override void Update(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); - + + SlideshowPlayer?.UpdateManually(deltaTime); + Map?.Radiation?.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || @@ -594,11 +560,19 @@ namespace Barotrauma CampaignUI.SelectTab(InteractionType.Map); } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } else { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost - if (!Submarine.MainSub.AtStartExit) + if (!Submarine.MainSub.AtStartExit && !Level.Loaded.StartOutpost.ExitPoints.Any()) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); @@ -608,11 +582,11 @@ namespace Barotrauma else { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation) { - EndCampaign(); + LoadNewLevel(); } - if (transitionType == TransitionType.ProgressToNextLocation && + else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { LoadNewLevel(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index fcb5438dc..49f2a6fcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -585,7 +585,7 @@ namespace Barotrauma if (!gap.IsRoomToRoom) { if (!IsWearingDivingSuit()) { continue; } - if (Character.Controlled.IsProtectedFromPressure()) { continue; } + if (Character.Controlled.IsProtectedFromPressure) { continue; } if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 232e84838..3b4d31cf6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -11,6 +11,8 @@ namespace Barotrauma { internal sealed partial class MedicalClinic { + private MedicalClinicUI? ui => campaign?.CampaignUI?.MedicalClinic; + public enum RequestResult { Undecided, @@ -303,6 +305,12 @@ namespace Barotrauma } } + private void AfflictionUpdateReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + ui?.UpdateAfflictions(crewMember); + } + private void PendingRequestReceived(IReadMessage inc) { var pendingCrew = INetSerializableStruct.Read>(inc); @@ -312,6 +320,10 @@ namespace Barotrauma } } + public static void SendUnsubscribeRequest() => ClientSend(null, + header: NetworkHeader.UNSUBSCRIBE_ME, + deliveryMethod: DeliveryMethod.Reliable); + private static IWriteMessage StartSending() { IWriteMessage writeMessage = new WriteOnlyMessage(); @@ -337,6 +349,9 @@ namespace Barotrauma case NetworkHeader.REQUEST_AFFLICTIONS: AfflictionRequestReceived(inc); break; + case NetworkHeader.AFFLICTION_UPDATE: + AfflictionUpdateReceived(inc); + break; case NetworkHeader.REQUEST_PENDING: PendingRequestReceived(inc); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index 9da10c685..cabf5bcbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -40,7 +40,7 @@ namespace Barotrauma private void CreateMessageBox(string author) { - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); + Vector2 relativeSize = new Vector2(0.3f * GUI.AspectRatioAdjustment, 0.15f); Point minSize = new Point(300, 200); msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index da2c6b35b..1f55a4ff8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -21,8 +21,7 @@ namespace Barotrauma private readonly GameMode gameMode; - private readonly float initialLocationReputation; - private readonly Dictionary initialFactionReputations = new Dictionary(); + private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } @@ -36,12 +35,11 @@ namespace Barotrauma this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; - initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; if (gameMode is CampaignMode campaignMode) { foreach (Faction faction in campaignMode.Factions) { - initialFactionReputations.Add(faction, faction.Reputation.Value); + initialFactionReputations.Add(faction.Prefab.Identifier, faction.Reputation.Value); } } } @@ -214,11 +212,12 @@ namespace Barotrauma Stretch = true }; - List missionsToDisplay = new List(selectedMissions); + List missionsToDisplay = new List(selectedMissions.Where(m => m.Prefab.ShowInMenus)); if (!selectedMissions.Any() && startLocation != null) { foreach (Mission mission in startLocation.SelectedMissions) { + if (!mission.Prefab.ShowInMenus) { continue; } if (mission.Locations[0] == mission.Locations[1] || mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) { @@ -312,18 +311,27 @@ namespace Barotrauma } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(missionMessage), wrap: true); - int reward = displayedMission.GetReward(Submarine.MainSub); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) + RichString reputationText = displayedMission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); - if (share > 0) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText); + } + + int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); + if (totalReward > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); - RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(totalReward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } } } } @@ -401,33 +409,17 @@ namespace Barotrauma }; reputationList.ContentBackground.Color = Color.Transparent; - if (startLocation.Type.HasOutpost && startLocation.Reputation != null) - { - var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); - var locationFrame = CreateReputationElement( - reputationList.Content, - startLocation.Name, - startLocation.Reputation.Value, startLocation.Reputation.NormalizedValue, initialLocationReputation, - startLocation.Type.Name, "", - iconStyle?.GetDefaultSprite(), startLocation.Type.GetPortrait(0), iconStyle?.Color ?? Color.White); - CreatePathUnlockElement(locationFrame, null, startLocation); - } - foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; - if (initialFactionReputations.ContainsKey(faction)) - { - initialReputation = initialFactionReputations[faction]; - } - else + if (!initialFactionReputations.TryGetValue(faction.Prefab.Identifier, out initialReputation)) { DebugConsole.AddWarning($"Could not determine reputation change for faction \"{faction.Prefab.Name}\" (faction was not present at the start of the round)."); } var factionFrame = CreateReputationElement( reputationList.Content, faction.Prefab.Name, - faction.Reputation.Value, faction.Reputation.NormalizedValue, initialReputation, + faction.Reputation, initialReputation, faction.Prefab.ShortDescription, faction.Prefab.Description, faction.Prefab.Icon, faction.Prefab.BackgroundPortrait, faction.Prefab.IconColor); CreatePathUnlockElement(factionFrame, faction, null); @@ -455,52 +447,60 @@ namespace Barotrauma void CreatePathUnlockElement(GUIComponent reputationFrame, Faction faction, Location location) { - if (GameMain.GameSession?.Campaign?.Map != null) + if (GameMain.GameSession?.Campaign?.Map == null) { return; } + + IEnumerable connectionsBetweenBiomes = + GameMain.GameSession.Campaign.Map.Connections.Where(c => c.Locations[0].Biome != c.Locations[1].Biome); + + foreach (LocationConnection connection in connectionsBetweenBiomes) { - foreach (LocationConnection connection in GameMain.GameSession.Campaign.Map.Connections) + if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + + //don't show the "reputation required to unlock" text if another connection between the biomes has already been unlocked + if (connectionsBetweenBiomes.Where(c => !c.Locked).Any(c => + (c.Locations[0].Biome == connection.Locations[0].Biome && c.Locations[1].Biome == connection.Locations[1].Biome) || + (c.Locations[1].Biome == connection.Locations[0].Biome && c.Locations[0].Biome == connection.Locations[1].Biome))) { - if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { continue; } + continue; + } - var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); - if (unlockEvent == null) { continue; } - if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (unlockEvent == null) { continue; } + if (unlockEvent.Faction.IsEmpty) + { + if (location == null || gateLocation != location) { continue; } + } + else + { + if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } + } + + if (unlockEvent != null) + { + Reputation unlockReputation = gateLocation.Reputation; + Faction unlockFaction = null; + if (!unlockEvent.Faction.IsEmpty) { - if (location == null || gateLocation != location) { continue; } + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); + unlockReputation = unlockFaction?.Reputation; } - else + float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( + "lockedpathreputationrequirement", + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); + var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); + unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); + unlockInfoPanel.UserData = "unlockinfo"; + if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } - } - - if (unlockEvent != null) - { - Reputation unlockReputation = gateLocation.Reputation; - Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) - { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); - unlockReputation = unlockFaction?.Reputation; - } - float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - RichString unlockText = RichString.Rich(TextManager.GetWithVariables( - "lockedpathreputationrequirement", - ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); - var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); - unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); - unlockInfoPanel.UserData = "unlockinfo"; - if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) - { - unlockInfoPanel.Font = GUIStyle.SmallFont; - } + unlockInfoPanel.Font = GUIStyle.SmallFont; } } - } + } } } @@ -543,6 +543,11 @@ namespace Barotrauma } } + if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) + { + locationName ??= startLocation.Name; + } + if (textTag == null) { return ""; } if (locationName == null) @@ -680,7 +685,7 @@ namespace Barotrauma } private GUIFrame CreateReputationElement(GUIComponent parent, - LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString name, Reputation reputation, float initialReputation, LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -698,21 +703,22 @@ namespace Barotrauma }; } - var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft, isHorizontal: true) + var factionInfoHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), factionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterRight, isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; + var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) + { + Color = iconColor + }; var factionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, factionInfoHorizontal.RectTransform)) { AbsoluteSpacing = GUI.IntScale(10), Stretch = true }; - var factionIcon = new GUIImage(new RectTransform(Vector2.One * 0.7f, factionInfoHorizontal.RectTransform, scaleBasis: ScaleBasis.Smallest), icon, scaleToFit: true) - { - Color = iconColor - }; + factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), @@ -733,24 +739,30 @@ namespace Barotrauma factionTextContent.Recalculate(); new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), - onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); + onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, reputation.NormalizedValue)); + + var reputationText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), + string.Empty, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + SetReputationText(reputationText); + reputation?.OnReputationValueChanged.RegisterOverwriteExisting("RefreshRoundSummary".ToIdentifier(), _ => + { + SetReputationText(reputationText); + }); - LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); - int reputationChange = (int)Math.Round(reputation - initialReputation); - if (Math.Abs(reputationChange) > 0) + void SetReputationText(GUITextBlock textBlock) { - string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); - var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - richText, - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - RichString.Rich(reputationText), - textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + LocalizedString reputationText = Reputation.GetFormattedReputationText(reputation.NormalizedValue, reputation.Value, addColorTags: true); + int reputationChange = (int)Math.Round(reputation.Value - initialReputation); + if (Math.Abs(reputationChange) > 0) + { + string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + textBlock.Text = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); + } + else + { + textBlock.Text = RichString.Rich(reputationText); + } } //spacing diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 18139a698..e388ac515 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -63,7 +63,6 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; - public static int Spacing; private Layout layout; public Layout CurrentLayout @@ -103,7 +102,7 @@ namespace Barotrauma { visualSlots ??= new VisualSlot[capacity]; - float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; + float multiplier = UIScale * GUI.AspectRatioAdjustment; for (int i = 0; i < capacity; i++) { @@ -219,18 +218,11 @@ namespace Barotrauma private void SetSlotPositions(Layout layout) { - bool isFourByThree = GUI.IsFourByThree(); - if (isFourByThree) - { - Spacing = (int)(5 * UIScale); - } - else - { - Spacing = (int)(8 * UIScale); - } + int spacing = GUI.IntScale(5); - SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); - int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; + SlotSize = (SlotSpriteSmall.size * UIScale * GUI.AspectRatioAdjustment).ToPoint(); + int bottomOffset = SlotSize.Y + spacing * 2 + ContainedIndicatorHeight; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - spacing * 2 - (int)(UnequippedIndicator.size.Y * UIScale); if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } @@ -242,11 +234,11 @@ namespace Barotrauma int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); - int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing; + int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + spacing) / 2; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - spacing; //make sure the rightmost normal slot doesn't overlap with the personal slots - x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); + x -= Math.Max((x + normalSlotCount * (SlotSize.X + spacing)) - (upperX - personalSlotCount * (SlotSize.X + spacing)), 0); int hideButtonSlotIndex = -1; for (int i = 0; i < SlotPositions.Length; i++) @@ -254,7 +246,7 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(upperX, GameMain.GraphicsHeight - bottomOffset); - upperX -= SlotSize.X + Spacing; + upperX -= SlotSize.X + spacing; personalSlotArea = (hideButtonSlotIndex == -1) ? new Rectangle(SlotPositions[i].ToPoint(), SlotSize) : Rectangle.Union(personalSlotArea, new Rectangle(SlotPositions[i].ToPoint(), SlotSize)); @@ -263,7 +255,7 @@ namespace Barotrauma else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += SlotSize.X + Spacing; + x += SlotSize.X + spacing; } } } @@ -271,7 +263,7 @@ namespace Barotrauma case Layout.Right: { int x = HUDLayoutSettings.InventoryAreaLower.Right; - int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; + int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - spacing; for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } @@ -282,19 +274,18 @@ namespace Barotrauma } else { - x -= SlotSize.X + Spacing; + x -= SlotSize.X + spacing; } } int lowerX = x; int handSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { SlotPositions[i] = new Vector2(handSlotX, personalSlotY); - handSlotX += visualSlots[i].Rect.Width + Spacing; + handSlotX += visualSlots[i].Rect.Width + spacing; continue; } @@ -302,12 +293,12 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX -= visualSlots[i].Rect.Width + Spacing; + personalSlotX -= visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } @@ -316,7 +307,7 @@ namespace Barotrauma { if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } - x -= visualSlots[i].Rect.Width + Spacing; + x -= visualSlots[i].Rect.Width + spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } @@ -325,7 +316,6 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; - int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { @@ -334,33 +324,33 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX += visualSlots[i].Rect.Width + Spacing; + personalSlotX += visualSlots[i].Rect.Width + spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - int handSlotX = x - visualSlots[0].Rect.Width - Spacing; + int handSlotX = x - visualSlots[0].Rect.Width - spacing; for (int i = 0; i < SlotPositions.Length; i++) { if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { bool rightSlot = SlotTypes[i] == InvSlotType.RightHand; - SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); + SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - spacing, personalSlotY); continue; } if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } break; case Layout.Center: { int columns = 5; - int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + Spacing * (columns - 1)) / 2; + int startX = (GameMain.GraphicsWidth / 2) - (SlotSize.X * columns + spacing * (columns - 1)) / 2; int startY = GameMain.GraphicsHeight / 2 - (SlotSize.Y * 2); int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) @@ -369,10 +359,10 @@ namespace Barotrauma if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; } } - y += visualSlots[0].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; + y += visualSlots[0].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; x = startX; int n = 0; for (int i = 0; i < SlotPositions.Length; i++) @@ -381,12 +371,12 @@ namespace Barotrauma if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += visualSlots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + spacing; n++; if (n >= columns) { x = startX; - y += visualSlots[i].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; + y += visualSlots[i].Rect.Height + spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; n = 0; } } @@ -402,7 +392,7 @@ namespace Barotrauma { if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } SlotPositions[i] = pos; - pos.Y += visualSlots[i].Rect.Height + Spacing; + pos.Y += visualSlots[i].Rect.Height + spacing; } } @@ -641,7 +631,7 @@ namespace Barotrauma { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { slot.EquipButtonState = GUIComponent.ComponentState.None; } @@ -1018,7 +1008,47 @@ namespace Barotrauma SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); } } - + + public bool CanBeAutoMovedToCorrectSlots(Item item) + { + if (item == null) { return false; } + foreach (var allowedSlot in item.AllowedSlots) + { + InvSlotType slotsFree = InvSlotType.None; + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } + } + if (allowedSlot == slotsFree) { return true; } + } + return false; + } + + /// + /// Flash the slots the item is allowed to go in (not taking into account whether there's already something in those slots) + /// + public void FlashAllowedSlots(Item item, Color color) + { + if (item == null || visualSlots == null) { return; } + bool flashed = false; + foreach (var allowedSlot in item.AllowedSlots) + { + for (int i = 0; i < slots.Length; i++) + { + if (allowedSlot.HasFlag(SlotTypes[i])) + { + visualSlots[i].ShowBorderHighlight(color, 0.1f, 0.9f); + flashed = true; + } + } + } + if (flashed) + { + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } + } + + public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } @@ -1106,40 +1136,24 @@ namespace Barotrauma color *= 0.5f; } - if (character.HasEquippedItem(slots[i].First())) + Vector2 indicatorScale = new Vector2( + visualSlots[i].EquipButtonRect.Size.X / EquippedIndicator.size.X, + visualSlots[i].EquipButtonRect.Size.Y / EquippedIndicator.size.Y); + + bool isEquipped = character.HasEquippedItem(slots[i].First()); + var sprite = state switch { - switch (state) - { - case GUIComponent.ComponentState.None: - EquippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - EquippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - EquippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } - else - { - switch (state) - { - case GUIComponent.ComponentState.None: - UnequippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Hover: - UnequippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - case GUIComponent.ComponentState.Pressed: - case GUIComponent.ComponentState.Selected: - case GUIComponent.ComponentState.HoverSelected: - UnequippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); - break; - } - } + GUIComponent.ComponentState.None + => isEquipped ? EquippedIndicator : UnequippedIndicator, + GUIComponent.ComponentState.Hover + => isEquipped ? EquippedHoverIndicator : UnequippedHoverIndicator, + GUIComponent.ComponentState.Pressed + or GUIComponent.ComponentState.Selected + or GUIComponent.ComponentState.HoverSelected + => isEquipped ? EquippedClickedIndicator : UnequippedClickedIndicator, + _ => throw new NotImplementedException() + }; + sprite.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, indicatorScale); } if (Locked) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index f5faabb79..e5b132f89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -182,7 +182,7 @@ namespace Barotrauma.Items.Components if (brokenSprite == null) { //broken doors turn black if no broken sprite has been configured - color *= (item.Condition / item.MaxCondition); + color = color.Multiply(item.Condition / item.MaxCondition); color.A = 255; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index fd8f360eb..2f23d59d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,13 +1,10 @@ using Barotrauma.Particles; +using Barotrauma.Sounds; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using Barotrauma.IO; -using System.Text; -using System.Xml.Linq; -using Barotrauma.Sounds; using System.Linq; namespace Barotrauma.Items.Components @@ -169,11 +166,11 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { Vector2 particlePos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - float rotation = -item.body.Rotation; + float rotation = item.body.Rotation; if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } foreach (ParticleEmitter emitter in particleEmitters) { - emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: rotation); + emitter.Emit(1.0f, particlePos, hullGuess: item.CurrentHull, angle: rotation, particleRotation: -rotation); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 345411caa..ebd078a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -505,13 +505,14 @@ namespace Barotrauma.Items.Components } ActionType type; + string typeStr = subElement.GetAttributeString("type", ""); try { - type = (ActionType)Enum.Parse(typeof(ActionType), subElement.GetAttributeString("type", ""), true); + type = (ActionType)Enum.Parse(typeof(ActionType), typeStr, true); } catch (Exception e) { - DebugConsole.ThrowError("Invalid sound type in " + subElement + "!", e); + DebugConsole.ThrowError($"Invalid sound type \"{typeStr}\" in item \"{item.Prefab.Identifier}\"!", e); break; } @@ -524,11 +525,13 @@ namespace Barotrauma.Items.Components VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; - if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); + if (soundSelectionModes == null) + { + soundSelectionModes = new Dictionary(); + } if (!soundSelectionModes.ContainsKey(type) || soundSelectionModes[type] == SoundSelectionMode.Random) { - Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode); - soundSelectionModes[type] = selectionMode; + soundSelectionModes[type] = subElement.GetAttributeEnum("selectionmode", SoundSelectionMode.Random); } if (!sounds.TryGetValue(itemSound.Type, out List soundList)) @@ -584,6 +587,8 @@ namespace Barotrauma.Items.Components { if (GuiFrame != null && GuiFrameSource.GetAttributeBool("draggable", true)) { + bool hideDragIcons = GuiFrameSource.GetAttributeBool("hidedragicons", false); + var handle = new GUIDragHandle(new RectTransform(Vector2.One, GuiFrame.RectTransform, Anchor.Center), GuiFrame.RectTransform, style: null) { @@ -623,7 +628,7 @@ namespace Barotrauma.Items.Components }; int buttonHeight = (int)(GUIStyle.ItemFrameMargin.Y * 0.4f); - new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, + var settingsIcon = new GUIButton(new RectTransform(new Point(buttonHeight), handle.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(buttonHeight / 4), MinSize = new Point(buttonHeight) }, style: "GUIButtonSettings") { OnClicked = (btn, userdata) => @@ -648,6 +653,12 @@ namespace Barotrauma.Items.Components return true; } }; + + if (hideDragIcons) + { + dragIcon.Visible = false; + settingsIcon.Visible = false; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5b16c7d9c..17293bf7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -277,7 +277,8 @@ namespace Barotrauma.Items.Components int ignoredItemCount = 0; var subContainableItems = AllSubContainableItems; - float capacity = GetMaxStackSize(targetSlot); + float targetSlotCapacity = GetMaxStackSize(targetSlot); + float capacity = targetSlotCapacity * MainContainerCapacity; if (subContainableItems != null) { bool useMainContainerCapacity = true; @@ -299,15 +300,11 @@ namespace Barotrauma.Items.Components } if (!useMainContainerCapacity) { break; } } - if (useMainContainerCapacity) - { - capacity *= MainContainerCapacity; - } - else + if (!useMainContainerCapacity) { // Ignore all items in the main container. ignoredItemCount = Inventory.AllItems.Count(it => subContainableItems.Any(ri => !ri.MatchesItem(it))); - capacity *= Capacity - MainContainerCapacity; + capacity = targetSlotCapacity * (Capacity - MainContainerCapacity); } } int itemCount = Inventory.AllItems.Count() - ignoredItemCount; @@ -391,63 +388,60 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (Item containedItem in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(containedItem); - if (relatedItem != null) + + if (contained.Item?.Sprite == null) { continue; } + + if (contained.Hide) { continue; } + if (contained.ItemPos.HasValue) { - if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.DrawPosition; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (item.Submarine != null) - { - itemPos += item.Submarine.DrawPosition; - } - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (item.Submarine != null) + { + itemPos += item.Submarine.DrawPosition; + } + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(-item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.DrawPosition, transform) + item.DrawPosition; } } } - - if (containedItem?.Sprite == null) { continue; } - + if (AutoInteractWithContained) { - containedItem.IsHighlighted = item.IsHighlighted; + contained.Item.IsHighlighted = item.IsHighlighted; item.IsHighlighted = false; } - Vector2 origin = containedItem.Sprite.Origin; - if (item.FlippedX) { origin.X = containedItem.Sprite.SourceRect.Width - origin.X; } - if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } + Vector2 origin = contained.Item.Sprite.Origin; + if (item.FlippedX) { origin.X = contained.Item.Sprite.SourceRect.Width - origin.X; } + if (item.FlippedY) { origin.Y = contained.Item.Sprite.SourceRect.Height - origin.Y; } - float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? contained.Item.Sprite.Depth : ContainedSpriteDepth; if (i < containedSpriteDepths.Length) { containedSpriteDepth = containedSpriteDepths[i]; @@ -456,9 +450,9 @@ namespace Barotrauma.Items.Components SpriteEffects spriteEffects = SpriteEffects.None; float spriteRotation = ItemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - spriteRotation = relatedItem.Rotation; + spriteRotation = contained.Rotation; } if ((item.body != null && item.body.Dir == -1) || item.FlippedX) { @@ -469,17 +463,17 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } - containedItem.Sprite.Draw( + contained.Item.Sprite.Draw( spriteBatch, new Vector2(itemPos.X, -itemPos.Y), - isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), + isWiringMode ? contained.Item.GetSpriteColor(withHighlight: true) * 0.15f : contained.Item.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), - containedItem.Scale, + -(contained.Item.body == null ? 0.0f : contained.Item.body.DrawRotation), + contained.Item.Scale, spriteEffects, depth: containedSpriteDepth); - foreach (ItemContainer ic in containedItem.GetComponents()) + foreach (ItemContainer ic in contained.Item.GetComponents()) { if (ic.hideItems) { continue; } ic.DrawContainedItems(spriteBatch, containedSpriteDepth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 2c764937c..b29eb3176 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -14,6 +14,13 @@ namespace Barotrauma.Items.Components private CoroutineHandle resetPredictionCoroutine; private float resetPredictionTimer; + /// + /// The current multiplier for the light color (usually equal to , but in the case of e.g. blinking lights the multiplier + /// doesn't go to 0 when the light turns off, because otherwise it'd take a while for it turn back on based on the lightBrightness which is interpolated + /// towards the current voltage). + /// + private float lightColorMultiplier; + public Vector2 DrawSize { get { return new Vector2(Light.Range * 2, Light.Range * 2); } @@ -27,21 +34,14 @@ namespace Barotrauma.Items.Components Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } - partial void SetLightSourceState(bool enabled, float? brightness) + partial void SetLightSourceState(bool enabled, float brightness) { if (Light == null) { return; } Light.Enabled = enabled; - if (brightness.HasValue) - { - lightBrightness = brightness.Value; - } - else - { - lightBrightness = enabled ? 1.0f : 0.0f; - } + lightColorMultiplier = brightness; if (enabled) { - Light.Color = LightColor.Multiply(lightBrightness); + Light.Color = LightColor.Multiply(lightColorMultiplier); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 3debe66f7..8e30123bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -185,6 +185,7 @@ namespace Barotrauma.Items.Components RefreshActivateButtonText(); if (GameMain.Client != null) { + pendingFabricatedItem = null; item.CreateClientEvent(this); } return true; @@ -336,8 +337,11 @@ namespace Barotrauma.Items.Components int calculatePlacement(FabricationRecipe recipe) { + if (recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem)) + { + return -2; + } int placement = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f ? 0 : -1; - placement += recipe.RequiresRecipe && !AnyOneHasRecipeForItem(character, recipe.TargetItem) ? -2 : 0; return placement; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index f901ec9ea..abe3fd223 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -298,7 +298,7 @@ namespace Barotrauma.Items.Components } } - OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).OrderBy(o => o.Identifier).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -452,7 +452,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, component) in electricalMapComponents) { GUIComponent parent = component.RectComponent; - if (!(entity is Item it )) { continue; } + if (entity is not Item it ) { continue; } Sprite? sprite = it.Prefab.UpgradePreviewSprite; if (sprite is null) { continue; } @@ -476,7 +476,7 @@ namespace Barotrauma.Items.Components { if (!hullPointsOfInterest.Contains(entity)) { continue; } - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } const int borderMaxSize = 2; if (it.GetComponent() is { }) @@ -643,7 +643,7 @@ namespace Barotrauma.Items.Components elementSize = GuiFrame.Rect.Size; } - float distort = 1.0f - item.Condition / item.MaxCondition; + float distort = item.Repairables.Any(r => r.IsBelowRepairThreshold) ? 1.0f - item.Condition / item.MaxCondition : 0.0f; foreach (HullData hullData in hullDatas.Values) { hullData.DistortionTimer -= deltaTime; @@ -1130,7 +1130,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { - if (!(entity is Item it)) { continue; } + if (entity is not Item it) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) @@ -1220,7 +1220,7 @@ namespace Barotrauma.Items.Components { foreach (var (entity, component) in hullStatusComponents) { - if (!(entity is Hull hull)) { continue; } + if (entity is not Hull hull) { continue; } if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; } if (hullData.Distort) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 628f5a54b..628294d1d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -17,6 +17,7 @@ namespace Barotrauma.Items.Components Default, Disruption, Destructible, + Door, LongRange } @@ -110,6 +111,10 @@ namespace Barotrauma.Items.Components BlipType.Destructible, new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } }, + { + BlipType.Door, + new Color[] { Color.TransparentBlack, new Color(73, 78, 86), new Color(66, 94, 100), new Color(47, 115, 58), new Color(255, 255, 255) } + }, { BlipType.LongRange, new Color[] { Color.TransparentBlack, Color.TransparentBlack, new Color(254, 68, 19) * 0.8f, Color.TransparentBlack } @@ -975,7 +980,7 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession == null || Level.Loaded == null) { return; } - if (Level.Loaded.StartLocation != null) + if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true }) { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, @@ -985,7 +990,7 @@ namespace Barotrauma.Items.Components displayScale, center, DisplayRadius); } - if (Level.Loaded.EndLocation != null && Level.Loaded.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection }) { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, @@ -1010,19 +1015,19 @@ namespace Barotrauma.Items.Components int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { - if (!mission.SonarLabel.IsNullOrWhiteSpace()) + int i = 0; + foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels) { - int i = 0; - foreach (Vector2 sonarPosition in mission.SonarPositions) + if (!string.IsNullOrEmpty(label.Value)) { DrawMarker(spriteBatch, - mission.SonarLabel.Value, + label.Value, mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, - sonarPosition, transducerCenter, + position, transducerCenter, displayScale, center, DisplayRadius * 0.95f); - i++; } + i++; } missionIndex++; } @@ -1176,13 +1181,18 @@ namespace Barotrauma.Items.Components if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's - if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } + if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && + !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon) + { + continue; + } //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && - dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) + !dockingPort.Item.Submarine.Info.IsOutpost && + !dockingPort.Item.Submarine.Info.IsBeacon) { // specifically checking for friendlyNPC seems more logical here if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } @@ -1348,6 +1358,38 @@ namespace Barotrauma.Items.Components } } + public void RegisterExplosion(Explosion explosion, Vector2 worldPosition) + { + if (Character.Controlled?.SelectedItem != item) { return; } + if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; } + Vector2 transducerCenter = GetTransducerPos(); + if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; } + int blipCount = MathHelper.Clamp((int)(explosion.Attack.Range / 100.0f), 0, 50); + for (int i = 0; i < blipCount; i++) + { + sonarBlips.Add(new SonarBlip( + worldPosition + Rand.Vector(Rand.Range(0.0f, explosion.Attack.Range)), + 1.0f, + Rand.Range(0.5f, 1.0f), + BlipType.Disruption)); + } + if (explosion.EmpStrength > 0.0f) + { + int empBlipCount = MathHelper.Clamp((int)(blipCount * explosion.EmpStrength), 10, 50); + for (int i = 0; i < empBlipCount; i++) + { + Vector2 dir = Rand.Vector(1.0f); + var longRangeBlip = new SonarBlip(worldPosition, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange) + { + Velocity = dir * MathUtils.Round(Rand.Range(4000.0f, 6000.0f), 1000.0f), + Rotation = (float)Math.Atan2(-dir.Y, dir.X) + }; + longRangeBlip.Size.Y *= 4.0f; + sonarBlips.Add(longRangeBlip); + } + } + } + private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive, float pingStrength = 1.0f, AITarget needsToBeInSector = null) { @@ -1392,6 +1434,14 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(submarine)) { continue; } } + Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); + worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, pingSource)) + { + CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); + continue; + } + for (int i = 0; i < submarine.HullVertices.Count; i++) { Vector2 start = ConvertUnits.ToDisplayUnits(submarine.HullVertices[i]); @@ -1608,6 +1658,40 @@ namespace Barotrauma.Items.Components } } + private void CreateBlipsForSubmarineWalls(Submarine sub, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float range, bool passive) + { + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != sub) { continue; } + CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect); + } + foreach (var door in Door.DoorList) + { + if (door.Item.Submarine != sub || door.IsOpen) { continue; } + CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, BlipType.Door); + } + + void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, BlipType blipType = BlipType.Default) + { + Vector2 point1, point2; + if (isHorizontal) + { + point1 = new Vector2(worldRect.X, worldPos.Y); + point2 = new Vector2(worldRect.Right, worldPos.Y); + } + else + { + point1 = new Vector2(worldPos.X, worldRect.Y); + point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height); + } + CreateBlipsForLine( + point1, + point2, + pingSource, transducerPos, + pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, blipType); + } + } + private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos) { Vector2 pos = (blip.Position - transducerPos) * displayScale * zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 3759e9fd1..c5e8531f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -340,6 +340,10 @@ namespace Barotrauma.Items.Components centerText = $"({TextManager.Get("Meter")})"; rightTextGetter = () => { + if (Level.Loaded is { IsEndBiome: true }) + { + return Timing.TotalTime % 5.0f < 0.5f ? Rand.Range(-9000, 9000).ToString() : "ERROR"; + } float realWorldDepth = controlledSub == null ? -1000.0f : controlledSub.RealWorldDepth; return ((int)realWorldDepth).ToString(); }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 28a25c88e..6a2b6571b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -20,6 +20,7 @@ namespace Barotrauma.Items.Components User = Entity.FindEntityByID(userId) as Character; Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); float rotation = msg.ReadSingle(); + SpreadCounter = msg.ReadByte(); if (User != null) { Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index afaeb2dca..c25a7c6cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -262,9 +262,11 @@ namespace Barotrauma.Items.Components } } + float conditionPercentage = item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f; + for (int i = 0; i < particleEmitters.Count; i++) { - if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) + if ((conditionPercentage >= particleEmitterConditionRanges[i].X && conditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } @@ -436,12 +438,16 @@ namespace Barotrauma.Items.Components ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; - item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); - if (CurrentFixer == null) + + if (CurrentFixer is null) { qteTimer = QteDuration; qteCooldown = 0.0f; } + else + { + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); + } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 9526f7f63..5bee8bba0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -108,6 +108,7 @@ namespace Barotrauma.Items.Components { startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } + startPos -= turret.GetRecoilOffset(); } else if (weapon != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 915e8f695..2525f4ffb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -110,8 +110,8 @@ namespace Barotrauma.Items.Components if (HighlightedWire != null) { HighlightedWire.Item.IsHighlighted = true; - if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) HighlightedWire.Connections[0].Item.IsHighlighted = true; - if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) HighlightedWire.Connections[1].Item.IsHighlighted = true; + if (HighlightedWire.Connections[0] != null && HighlightedWire.Connections[0].Item != null) { HighlightedWire.Connections[0].Item.IsHighlighted = true; } + if (HighlightedWire.Connections[1] != null && HighlightedWire.Connections[1].Item != null) { HighlightedWire.Connections[1].Item.IsHighlighted = true; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index ec3054df9..c879cb159 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; using System.Xml.Linq; @@ -24,7 +25,9 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element) { - var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }) + float marginMultiplier = element.GetAttributeFloat("marginmultiplier", 1.0f); + + var layoutGroup = new GUILayoutGroup(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin.Multiply(marginMultiplier), GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset.Multiply(marginMultiplier) }) { ChildAnchor = Anchor.TopCenter, RelativeSpacing = 0.02f, @@ -33,31 +36,34 @@ namespace Barotrauma.Items.Components historyBox = new GUIListBox(new RectTransform(new Vector2(1, .9f), layoutGroup.RectTransform), style: null) { - AutoHideScrollBar = false + AutoHideScrollBar = this.AutoHideScrollbar }; - CreateFillerBlock(); - - new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); - - inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) + if (!Readonly) { - MaxTextLength = MaxMessageLength, - OverflowClip = true, - OnEnterPressed = (GUITextBox textBox, string text) => + CreateFillerBlock(); + + new GUIFrame(new RectTransform(new Vector2(0.9f, 0.01f), layoutGroup.RectTransform), style: "HorizontalLine"); + + inputBox = new GUITextBox(new RectTransform(new Vector2(1, .1f), layoutGroup.RectTransform), textColor: TextColor) { - if (GameMain.NetworkMember == null) + MaxTextLength = MaxMessageLength, + OverflowClip = true, + OnEnterPressed = (GUITextBox textBox, string text) => { - SendOutput(text); + if (GameMain.NetworkMember == null) + { + SendOutput(text); + } + else + { + item.CreateClientEvent(this, new ClientEventData(text)); + } + textBox.Text = string.Empty; + return true; } - else - { - item.CreateClientEvent(this, new ClientEventData(text)); - } - textBox.Text = string.Empty; - return true; - } - }; + }; + } layoutGroup.Recalculate(); } @@ -101,7 +107,7 @@ namespace Barotrauma.Items.Components GUITextBlock newBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0), historyBox.Content.RectTransform, anchor: Anchor.TopCenter), - "> " + input, + LineStartSymbol + TextManager.Get(input).Fallback(input), textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.Font) { CanBeFocused = false @@ -123,7 +129,10 @@ namespace Barotrauma.Items.Components historyBox.RecalculateChildren(); historyBox.UpdateScrollBarSize(); - historyBox.ScrollBar.BarScrollValue = 1; + if (AutoScrollToBottom) + { + historyBox.ScrollBar.BarScrollValue = 1; + } } public override bool Select(Character character) @@ -138,7 +147,7 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList(int order = 0) { base.AddToGUIUpdateList(order: order); - if (shouldSelectInputBox) + if (shouldSelectInputBox && !Readonly) { inputBox.Select(); shouldSelectInputBox = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index afe938020..9924b8ad8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -302,7 +302,18 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } + if (affliction.Strength <= 0f) { continue; } + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold) + { + if (target.IsHuman || target.IsOnPlayerTeam || (affliction.Prefab.AfflictionType != AfflictionPrefab.PoisonType && affliction.Prefab.AfflictionType != AfflictionPrefab.ParalysisType)) + { + // Always show the poisons on monsters, because poisoning bigger monsters require multiple doses. + // The solution is hacky, but didn't want to introduce an extra property for this. + // We also want to have a relatively high thershold for showing the poisons on the scanner on humans, so that it's not instantly clear that a target is poisoned and especially not which poison was used. + // Paralysis is treated like a poison but isn't technically a poison, so that we can have multiple afflictions that still are treated the same. + continue; + } + } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index e66a15de4..402805f70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -333,15 +333,9 @@ namespace Barotrauma.Items.Components crosshairPointerPos = PlayerInput.MousePosition; } - - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) - { - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) - { - UpdateTransformedBarrelPos(); - } - Vector2 drawPos = GetDrawPos(); + public Vector2 GetRecoilOffset() + { float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { @@ -362,6 +356,17 @@ namespace Barotrauma.Items.Components recoilOffset = RecoilDistance; } } + return new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset; + } + + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) + { + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) + { + UpdateTransformedBarrelPos(); + } + Vector2 drawPos = GetDrawPos(); + railSprite?.Draw(spriteBatch, drawPos, @@ -370,7 +375,7 @@ namespace Barotrauma.Items.Components SpriteEffects.None, item.SpriteDepth + (railSprite.Depth - item.Sprite.Depth)); barrelSprite?.Draw(spriteBatch, - drawPos - new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)) * recoilOffset * item.Scale, + drawPos - GetRecoilOffset() * item.Scale, item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 5b4d60065..96607abbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -53,11 +53,11 @@ namespace Barotrauma get { // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen - if (IsMoving) return offScreenRect; + if (IsMoving) { return offScreenRect; } int buttonDir = Math.Sign(SubInventoryDir); - float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; + float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale; Vector2 equipIndicatorPos = new Vector2(Rect.Left, Rect.Center.Y + (Rect.Height / 2 + 15 * Inventory.UIScale) * buttonDir - sizeY / 2f); equipIndicatorPos += DrawOffset; @@ -176,14 +176,6 @@ namespace Barotrauma public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; - public static float IndicatorScaleAdjustment - { - get - { - return !GUI.IsFourByThree() ? 0.8f : 0.7f; - } - } - public static Inventory DraggingInventory; public Inventory ReplacedBy; @@ -249,11 +241,11 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - Tooltip = GetTooltip(Item, itemsInSlot); + Tooltip = GetTooltip(Item, itemsInSlot, Character.Controlled); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private RichString GetTooltip(Item item, IEnumerable itemsInSlot) + private static RichString GetTooltip(Item item, IEnumerable itemsInSlot, Character character) { if (item == null) { return null; } @@ -348,10 +340,12 @@ namespace Barotrauma } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + toolTip += $"\n‖color:gui.blue‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + } + if (item.Prefab.SkillRequirementHints != null && item.Prefab.SkillRequirementHints.Any()) + { + toolTip += item.Prefab.GetSkillRequirementHints(character); } return RichString.Rich(toolTip); } @@ -576,7 +570,7 @@ namespace Barotrauma } } - if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) + if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.SecondaryMouseButtonHeld()) { mouseOn = false; } @@ -727,14 +721,7 @@ namespace Barotrauma Rectangle subRect = slot.Rect; Vector2 spacing; - if (GUI.IsFourByThree()) - { - spacing = new Vector2(5 * UIScale, (5 + UnequippedIndicator.size.Y) * UIScale); - } - else - { - spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); - } + spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale * GUI.AspectRatioAdjustment); int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) @@ -1535,16 +1522,6 @@ namespace Barotrauma { Sprite slotSprite = slot.SlotSprite ?? SlotSpriteSmall; - /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null - && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) - { - slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; - } - else - { - slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; - }*/ - if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); @@ -1731,7 +1708,17 @@ namespace Barotrauma slot.InventoryKeyIndex < GameSettings.CurrentConfig.InventoryKeyMap.Bindings.Length) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); + + GUIStyle.HotkeyFont.DrawString( + spriteBatch, + GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, + rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), + Color.Black, + rotation: 0.0f, + origin: Vector2.Zero, + scale: Vector2.One * GUI.AspectRatioAdjustment, + SpriteEffects.None, + layerDepth: 0.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 684cb0d5c..2ffbf71ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -23,7 +23,21 @@ namespace Barotrauma private readonly List activeEditors = new List(); - public GUIComponentStyle IconStyle { get; private set; } = null; + + private GUIComponentStyle iconStyle; + public GUIComponentStyle IconStyle + { + get { return iconStyle; } + private set + { + if (IconStyle != value) + { + iconStyle = value; + CheckIsHighlighted(); + } + } + } + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { if (interactionType == CampaignMode.InteractionType.None) @@ -143,6 +157,18 @@ namespace Barotrauma return color; } + protected override void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight || IconStyle != null) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + public Color GetInventoryIconColor() { Color color = InventoryIconColor; @@ -281,7 +307,8 @@ namespace Barotrauma cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } - Vector2 worldPosition = WorldPosition; + Vector2 worldPosition = WorldPosition + GetCollapseEffectOffset(); + if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } @@ -310,7 +337,9 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; - Vector2 drawOffset = Vector2.Zero; + Vector2 drawOffset = GetCollapseEffectOffset(); + drawOffset.Y = -drawOffset.Y; + if (displayCondition < MaxCondition) { for (int i = 0; i < Prefab.BrokenSprites.Length; i++) @@ -426,6 +455,8 @@ namespace Barotrauma var holdable = GetComponent(); if (holdable != null && holdable.Picker?.AnimController != null) { + //don't draw the item on hands if it's also being worn + if (GetComponent() is { IsActive: true }) { return; } if (!back) { return; } float depthStep = 0.000001f; if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) @@ -728,7 +759,7 @@ namespace Barotrauma if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - var otherEntity = mapEntityList.FirstOrDefault(e => e != this && e.IsHighlighted && e.IsMouseOn(position)); + var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position)); if (otherEntity != null) { if (linkedTo.Contains(otherEntity)) @@ -1672,25 +1703,24 @@ namespace Barotrauma bool hasIdCard = msg.ReadBoolean(); string ownerName = "", ownerTags = ""; int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; - Color ownerHairColor = Microsoft.Xna.Framework.Color.White, - ownerFacialHairColor = Microsoft.Xna.Framework.Color.White, - ownerSkinColor = Microsoft.Xna.Framework.Color.White; + Color ownerHairColor = Color.White, + ownerFacialHairColor = Color.White, + ownerSkinColor = Color.White; Identifier ownerJobId = Identifier.Empty; Vector2 ownerSheetIndex = Vector2.Zero; + int submarineSpecificId = 0; if (hasIdCard) { + submarineSpecificId = msg.ReadInt32(); ownerName = msg.ReadString(); - ownerTags = msg.ReadString(); - + ownerTags = msg.ReadString(); ownerBeardIndex = msg.ReadByte() - 1; ownerHairIndex = msg.ReadByte() - 1; ownerMoustacheIndex = msg.ReadByte() - 1; - ownerFaceAttachmentIndex = msg.ReadByte() - 1; - + ownerFaceAttachmentIndex = msg.ReadByte() - 1; ownerHairColor = msg.ReadColorR8G8B8(); ownerFacialHairColor = msg.ReadColorR8G8B8(); - ownerSkinColor = msg.ReadColorR8G8B8(); - + ownerSkinColor = msg.ReadColorR8G8B8(); ownerJobId = msg.ReadIdentifier(); int x = msg.ReadByte(); @@ -1794,6 +1824,7 @@ namespace Barotrauma } foreach (IdCard idCard in item.GetComponents()) { + idCard.SubmarineSpecificID = submarineSpecificId; idCard.TeamID = (CharacterTeamType)teamID; idCard.OwnerName = ownerName; idCard.OwnerTags = ownerTags; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 17cd3e848..49c3eb0e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -254,7 +254,7 @@ namespace Barotrauma if (!DefaultPrice.RequiresUnlock) { return true; } return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); } - public LocalizedString GetTooltip() + public LocalizedString GetTooltip(Character character) { LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; if (!Description.IsNullOrEmpty()) @@ -265,6 +265,10 @@ namespace Barotrauma { Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); } + if (SkillRequirementHints != null && SkillRequirementHints.Any()) + { + tooltip += GetSkillRequirementHints(character); + } return tooltip; } @@ -376,5 +380,31 @@ namespace Barotrauma Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } + + public LocalizedString GetSkillRequirementHints(Character character) + { + LocalizedString text = ""; + if (SkillRequirementHints != null && SkillRequirementHints.Any() && character != null) + { + Color orange = GUIStyle.Orange; + // Reuse an existing, localized, text because it's what we want here: "Required skills:" + text = "\n\n" + $"‖color:{orange.ToStringHex()}‖{TextManager.Get("requiredrepairskills")}‖color:end‖"; + foreach (var hint in SkillRequirementHints) + { + int skillLevel = (int)character.GetSkillLevel(hint.Skill); + Color levelColor = GUIStyle.Yellow; + if (skillLevel >= hint.Level) + { + levelColor = GUIStyle.Green; + } + else if (skillLevel < hint.Level / 2) + { + levelColor = GUIStyle.Red; + } + text += "\n" + hint.GetFormattedText(skillLevel, levelColor.ToStringHex()); + } + } + return text; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 095195834..eac1396ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -129,20 +129,15 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted) { continue; } + if (entity == this) { continue; } if (!entity.IsMouseOn(position)) { continue; } if (entity.linkedTo == null || !entity.Linkable) { continue; } if (entity.linkedTo.Contains(this) || linkedTo.Contains(entity) || rClick) { - if (entity == this || !entity.IsHighlighted) { continue; } - if (!entity.IsMouseOn(position)) { continue; } - if (entity.linkedTo.Contains(this)) - { - entity.linkedTo.Remove(this); - linkedTo.Remove(entity); - } + entity.linkedTo.Remove(this); + linkedTo.Remove(entity); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 67a6e6fa2..38299827b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -114,7 +114,11 @@ namespace Barotrauma graphics.Clear(BackgroundColor); - renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + if (renderer != null) + { + GameMain.LightManager.AmbientLight = GameMain.LightManager.AmbientLight.Add(renderer.FlashColor); + renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + } } public void DrawFront(SpriteBatch spriteBatch, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index f92c7a30f..9366776a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -1,4 +1,4 @@ -using FarseerPhysics; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -71,9 +71,12 @@ namespace Barotrauma { private static BasicEffect wallEdgeEffect, wallCenterEffect; - private Vector2 dustOffset; - private Vector2 defaultDustVelocity; - private Vector2 dustVelocity; + private Vector2 waterParticleOffset; + private Vector2 waterParticleVelocity; + + private float flashCooldown; + private float flashTimer; + public Color FlashColor { get; private set; } private readonly RasterizerState cullNone; @@ -81,10 +84,26 @@ namespace Barotrauma private readonly List vertexBuffers = new List(); + private float chromaticAberrationStrength; + public float ChromaticAberrationStrength + { + get { return chromaticAberrationStrength; } + set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } + } + public float CollapseEffectStrength + { + get; + set; + } + public Vector2 CollapseEffectOrigin + { + get; + set; + } + + public LevelRenderer(Level level) { - defaultDustVelocity = Vector2.UnitY * 10.0f; - cullNone = new RasterizerState() { CullMode = CullMode.None }; if (wallEdgeEffect == null) @@ -120,12 +139,50 @@ namespace Barotrauma level.GenerationParams.WallSprite.ReloadTexture(); wallCenterEffect.Texture = level.GenerationParams.WallSprite.Texture; } + + public void Flash() + { + flashTimer = 1.0f; + } public void Update(float deltaTime, Camera cam) { + if (CollapseEffectStrength > 0.0f) + { + CollapseEffectStrength = Math.Max(0.0f, CollapseEffectStrength - deltaTime); + } + if (ChromaticAberrationStrength > 0.0f) + { + ChromaticAberrationStrength = Math.Max(0.0f, ChromaticAberrationStrength - deltaTime * 10.0f); + } + + if (level.GenerationParams.FlashInterval.Y > 0) + { + flashCooldown -= deltaTime; + if (flashCooldown <= 0.0f) + { + flashTimer = 1.0f; + if (level.GenerationParams.FlashSound != null) + { + level.GenerationParams.FlashSound.Play(1.0f, "default"); + } + flashCooldown = Rand.Range(level.GenerationParams.FlashInterval.X, level.GenerationParams.FlashInterval.Y, Rand.RandSync.Unsynced); + } + if (flashTimer > 0.0f) + { + float brightness = flashTimer * 1.1f - PerlinNoise.GetPerlin((float)Timing.TotalTime, (float)Timing.TotalTime * 0.66f) * 0.1f; + FlashColor = level.GenerationParams.FlashColor.Multiply(MathHelper.Clamp(brightness, 0.0f, 1.0f)); + flashTimer -= deltaTime * 0.5f; + } + else + { + FlashColor = Color.TransparentBlack; + } + } + //calculate the sum of the forces of nearby level triggers - //and use it to move the dust texture and water distortion effect - Vector2 currentDustVel = defaultDustVelocity; + //and use it to move the water texture and water distortion effect + Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity; foreach (LevelObject levelObject in level.LevelObjectManager.GetVisibleObjects()) { if (levelObject.Triggers == null) { continue; } @@ -139,21 +196,21 @@ namespace Barotrauma objectMaxFlow = vel; } } - currentDustVel += objectMaxFlow; + currentWaterParticleVel += objectMaxFlow; } + + waterParticleVelocity = Vector2.Lerp(waterParticleVelocity, currentWaterParticleVel, deltaTime); - dustVelocity = Vector2.Lerp(dustVelocity, currentDustVel, deltaTime); - - WaterRenderer.Instance?.ScrollWater(dustVelocity, deltaTime); + WaterRenderer.Instance?.ScrollWater(waterParticleVelocity, deltaTime); if (level.GenerationParams.WaterParticles != null) { Vector2 waterTextureSize = level.GenerationParams.WaterParticles.size * level.GenerationParams.WaterParticleScale; - dustOffset += new Vector2(dustVelocity.X, -dustVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; - while (dustOffset.X <= -waterTextureSize.X) dustOffset.X += waterTextureSize.X; - while (dustOffset.X >= waterTextureSize.X) dustOffset.X -= waterTextureSize.X; - while (dustOffset.Y <= -waterTextureSize.Y) dustOffset.Y += waterTextureSize.Y; - while (dustOffset.Y >= waterTextureSize.Y) dustOffset.Y -= waterTextureSize.Y; + waterParticleOffset += new Vector2(waterParticleVelocity.X, -waterParticleVelocity.Y) * level.GenerationParams.WaterParticleScale * deltaTime; + while (waterParticleOffset.X <= -waterTextureSize.X) { waterParticleOffset.X += waterTextureSize.X; } + while (waterParticleOffset.X >= waterTextureSize.X){ waterParticleOffset.X -= waterTextureSize.X; } + while (waterParticleOffset.Y <= -waterTextureSize.Y) { waterParticleOffset.Y += waterTextureSize.Y; } + while (waterParticleOffset.Y >= waterTextureSize.Y) { waterParticleOffset.Y -= waterTextureSize.Y; } } } @@ -234,7 +291,7 @@ namespace Barotrauma Rectangle srcRect = new Rectangle(0, 0, 2048, 2048); Vector2 origin = new Vector2(cam.WorldView.X, -cam.WorldView.Y); - Vector2 offset = -origin + dustOffset; + Vector2 offset = -origin + waterParticleOffset; while (offset.X <= -srcRect.Width * textureScale) offset.X += srcRect.Width * textureScale; while (offset.X > 0.0f) offset.X -= srcRect.Width * textureScale; while (offset.Y <= -srcRect.Height * textureScale) offset.Y += srcRect.Height * textureScale; @@ -261,7 +318,7 @@ namespace Barotrauma level.GenerationParams.WaterParticles.DrawTiled( spriteBatch, origin + offsetS, new Vector2(cam.WorldView.Width - offsetS.X, cam.WorldView.Height - offsetS.Y), - color: Color.White * alpha, textureScale: new Vector2(texScale)); + color: level.GenerationParams.WaterParticleColor * alpha, textureScale: new Vector2(texScale)); } } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 27b1aaf28..b4cf7304f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -2,7 +2,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using SharpFont; using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,28 +11,14 @@ namespace Barotrauma.Lights { class ConvexHullList { - private List list; - public HashSet IsHidden; public readonly Submarine Submarine; - public List List - { - get { return list; } - set - { - Debug.Assert(value != null); - Debug.Assert(!list.Contains(null)); - list = value; - IsHidden.RemoveWhere(ch => !list.Contains(ch)); - } - } - + public HashSet IsHidden = new HashSet(); + public readonly List List = new List(); public ConvexHullList(Submarine submarine) { Submarine = submarine; - list = new List(); - IsHidden = new HashSet(); } } @@ -354,7 +339,7 @@ namespace Barotrauma.Lights } } - public bool IsSegmentAInB(Segment a, Segment b) + public static bool IsSegmentAInB(Segment a, Segment b) { if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos)) { @@ -362,15 +347,16 @@ namespace Barotrauma.Lights } Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y)); - Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); min.X -= 1.0f; min.Y -= 1.0f; - max.X += 1.0f; max.Y += 1.0f; if (a.Start.Pos.X < min.X) { return false; } if (a.Start.Pos.Y < min.Y) { return false; } if (a.End.Pos.X < min.X) { return false; } if (a.End.Pos.Y < min.Y) { return false; } + Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y)); + max.X += 1.0f; max.Y += 1.0f; + if (a.Start.Pos.X > max.X) { return false; } if (a.Start.Pos.Y > max.Y) { return false; } if (a.End.Pos.X > max.X) { return false; } @@ -628,7 +614,7 @@ namespace Barotrauma.Lights { for (int i = 0; i < 4; i++) { - if (ignoreEdge[i] && ignoreEdges) continue; + if (ignoreEdge[i] && ignoreEdges) { continue; } Vector2 pos1 = vertices[i].WorldPos; Vector2 pos2 = vertices[(i + 1) % 4].WorldPos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index d8314b1b9..4d734fc23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -449,9 +449,9 @@ namespace Barotrauma.Lights { highlightedEntities.Add(Character.Controlled.FocusedCharacter); } - foreach (Item item in Item.ItemList) + foreach (MapEntity me in MapEntity.HighlightedEntities) { - if ((item.IsHighlighted || item.IconStyle != null) && !highlightedEntities.Contains(item)) + if (me is Item item && item != Character.Controlled.FocusedItem) { highlightedEntities.Add(item); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 7f981e74f..eaca44df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -203,7 +203,7 @@ namespace Barotrauma.Lights private VertexPositionColorTexture[] vertices; private short[] indices; - private readonly List hullsInRange; + private readonly List convexHullsInRange; public Texture2D texture; @@ -234,7 +234,7 @@ namespace Barotrauma.Lights { if (!needsRecalculation && value) { - foreach (ConvexHullList chList in hullsInRange) + foreach (ConvexHullList chList in convexHullsInRange) { chList.IsHidden.Clear(); } @@ -474,7 +474,7 @@ namespace Barotrauma.Lights public LightSource(Vector2 position, float range, Color color, Submarine submarine, bool addLight=true) { - hullsInRange = new List(); + convexHullsInRange = new List(); this.ParentSub = submarine; this.position = position; lightSourceParams = new LightSourceParams(range, color); @@ -515,19 +515,25 @@ namespace Barotrauma.Lights /// private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { - var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); + var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); if (fullChList == null) { return; } - chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); - - NeedsHullCheck = true; + chList.List.Clear(); + foreach (var convexHull in fullChList.List) + { + if (!convexHull.Enabled) { continue; } + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; } + chList.List.Add(convexHull); + } + chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); + NeedsHullCheck = false; } /// /// 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() + private void CheckConvexHullsInRange() { foreach (Submarine sub in Submarine.Loaded) { @@ -540,21 +546,13 @@ namespace Barotrauma.Lights private void CheckHullsInRange(Submarine sub) { //find the list of convexhulls that belong to the sub - ConvexHullList chList = null; - foreach (var ch in hullsInRange) - { - if (ch.Submarine == sub) - { - chList = ch; - break; - } - } - + ConvexHullList chList = convexHullsInRange.FirstOrDefault(chList => chList.Submarine == sub); + //not found -> create one if (chList == null) { chList = new ConvexHullList(sub); - hullsInRange.Add(chList); + convexHullsInRange.Add(chList); NeedsRecalculation = true; } @@ -646,6 +644,10 @@ namespace Barotrauma.Lights } } + private static readonly List visibleSegments = new List(); + private static readonly List points = new List(); + private static readonly List output = new List(); + private static readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; private List FindRaycastHits() { if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } @@ -653,12 +655,17 @@ namespace Barotrauma.Lights Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } - var hulls = new List(); - foreach (ConvexHullList chList in hullsInRange) + visibleSegments.Clear(); + foreach (ConvexHullList chList in convexHullsInRange) { foreach (ConvexHull hull in chList.List) { - if (!chList.IsHidden.Contains(hull)) { hulls.Add(hull); } + if (!chList.IsHidden.Contains(hull)) + { + //find convexhull segments that are close enough and facing towards the light source + hull.RefreshWorldPositions(); + hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); + } } foreach (ConvexHull hull in chList.List) { @@ -666,23 +673,13 @@ namespace Barotrauma.Lights } } - float bounds = TextureRange; - //find convexhull segments that are close enough and facing towards the light source - List visibleSegments = new List(); - List points = new List(); - foreach (ConvexHull hull in hulls) - { - hull.RefreshWorldPositions(); - hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); - } - //add a square-shaped boundary to make sure we've got something to construct the triangles from //even if there aren't enough hull segments around the light source //(might be more effective to calculate if we actually need these extra points) Vector2 drawOffset = Vector2.Zero; - float boundsExtended = bounds; + float boundsExtended = TextureRange; if (OverrideLightTexture != null) { float cosAngle = (float)Math.Cos(Rotation); @@ -706,12 +703,12 @@ namespace Barotrauma.Lights drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; } - var boundaryCorners = new SegmentPoint[] { - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X + boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y - boundsExtended), null), - new SegmentPoint(new Vector2(drawPos.X + drawOffset.X - boundsExtended, drawPos.Y + drawOffset.Y + boundsExtended), null) - }; + Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); + Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); + boundaryCorners[0] = new SegmentPoint(boundsMax, null); + boundaryCorners[1] = new SegmentPoint(new Vector2(boundsMax.X, boundsMin.Y), null); + boundaryCorners[2] = new SegmentPoint(boundsMin, null); + boundaryCorners[3] = new SegmentPoint(new Vector2(boundsMin.X, boundsMax.Y), null); for (int i = 0; i < 4; i++) { @@ -795,6 +792,7 @@ namespace Barotrauma.Lights } } + points.Clear(); //remove segments that fall out of bounds for (int i = 0; i < visibleSegments.Count; i++) { @@ -814,7 +812,18 @@ namespace Barotrauma.Lights } } - visibleSegments = visibleSegments.OrderBy(s => MathUtils.LineToPointDistanceSquared(s.Start.WorldPos, s.End.WorldPos, drawPos)).ToList(); + //remove points that are very close to each other + for (int i = 0; i < points.Count; i++) + { + for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) + { + if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && + Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) + { + points.RemoveAt(j); + } + } + } var compareCCW = new CompareSegmentPointCW(drawPos); try @@ -830,23 +839,12 @@ namespace Barotrauma.Lights } DebugConsole.ThrowError(sb.ToString(), e); } + + visibleSegments.Sort((s1, s2) => + MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos) + .CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos))); - List output = new List(); - //List> preOutput = new List>(); - - //remove points that are very close to each other - for (int i = 0; i < points.Count; i++) - { - for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) - { - if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && - Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) - { - points.RemoveAt(j); - } - } - } - + output.Clear(); foreach (SegmentPoint p in points) { Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos); @@ -854,10 +852,10 @@ namespace Barotrauma.Lights //do two slightly offset raycasts to hit the segment itself and whatever's behind it var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments); + if (intersection1.index < 0) { return null; } var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments); + if (intersection2.index < 0) { return null; } - if (intersection1.index < 0) return null; - if (intersection2.index < 0) return null; Segment seg1 = visibleSegments[intersection1.index]; Segment seg2 = visibleSegments[intersection2.index]; @@ -869,7 +867,7 @@ namespace Barotrauma.Lights //hit at the current segmentpoint -> place the segmentpoint into the list output.Add(p.WorldPos); - foreach (ConvexHullList hullList in hullsInRange) + foreach (ConvexHullList hullList in convexHullsInRange) { hullList.IsHidden.Remove(p.ConvexHull); hullList.IsHidden.Remove(seg1.ConvexHull); @@ -883,7 +881,7 @@ namespace Barotrauma.Lights output.Add(isPoint1 ? p.WorldPos : intersection1.pos); output.Add(isPoint2 ? p.WorldPos : intersection2.pos); - foreach (ConvexHullList hullList in hullsInRange) + foreach (ConvexHullList hullList in convexHullsInRange) { hullList.IsHidden.Remove(p.ConvexHull); hullList.IsHidden.Remove(seg1.ConvexHull); @@ -911,7 +909,7 @@ namespace Barotrauma.Lights return output; } - private (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) + private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List segments) { Vector2? closestIntersection = null; int segment = -1; @@ -936,13 +934,13 @@ namespace Barotrauma.Lights //same for the x-axis if (s.Start.WorldPos.X > s.End.WorldPos.X) { - if (s.Start.WorldPos.X < minX) continue; - if (s.End.WorldPos.X > maxX) continue; + if (s.Start.WorldPos.X < minX) { continue; } + if (s.End.WorldPos.X > maxX) { continue; } } else { - if (s.End.WorldPos.X < minX) continue; - if (s.Start.WorldPos.X > maxX) continue; + if (s.End.WorldPos.X < minX) { continue; } + if (s.Start.WorldPos.X > maxX) { continue; } } bool intersects; @@ -1335,7 +1333,7 @@ namespace Barotrauma.Lights return; } - CheckHullsInRange(); + CheckConvexHullsInRange(); if (NeedsRecalculation && allowRecalculation) { @@ -1387,7 +1385,7 @@ namespace Barotrauma.Lights public void Reset() { - hullsInRange.Clear(); + convexHullsInRange.Clear(); diffToSub.Clear(); NeedsHullCheck = true; NeedsRecalculation = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 573c648c4..2170143c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -70,9 +70,9 @@ namespace Barotrauma Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - foreach (MapEntity entity in mapEntityList) + foreach (MapEntity entity in HighlightedEntities) { - if (entity == this || !entity.IsHighlighted || !(entity is Item) || !entity.IsMouseOn(position)) { continue; } + if (entity == this|| entity is not Item || !entity.IsMouseOn(position)) { continue; } if (((Item)entity).GetComponent() == null) { continue; } if (linkedTo.Contains(entity)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index bdc94b8de..18f4b34a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework.Input; +using Barotrauma.Extensions; namespace Barotrauma { @@ -72,6 +73,8 @@ namespace Barotrauma private RichString beaconStationActiveText, beaconStationInactiveText; + private GUIComponent locationInfoOverlay; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -98,7 +101,7 @@ namespace Barotrauma OnClicked = (btn, userData) => { Rand.SetSyncedSeed(ToolBox.StringToInt(this.Seed)); - Generate(GameMain.GameSession.GameMode is CampaignMode campaign ? campaign.Settings : CampaignSettings.Empty); + Generate(GameMain.GameSession?.Campaign); InitProjectSpecific(); return true; } @@ -186,7 +189,7 @@ namespace Barotrauma private void LocationChanged(Location prevLocation, Location newLocation) { - if (prevLocation == newLocation) return; + if (prevLocation == newLocation) { return; } //focus on starting location if (prevLocation != null) { @@ -210,11 +213,17 @@ namespace Barotrauma currLocationIndicatorPos = CurrentLocation.MapPosition; } - RemoveFogOfWar(newLocation); + if (newLocation.Visited) + { + RemoveFogOfWar(newLocation); + } } + partial void RemoveFogOfWarProjSpecific(Location location) => RemoveFogOfWar(location); + private void RemoveFogOfWar(Location location, bool removeFromAdjacentLocations = true) { + if (mapTiles == null) { return; } if (location == null) { return; } var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); @@ -252,27 +261,223 @@ namespace Barotrauma return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)]; } + private class MapNotification + { + public readonly RichString Text; + public readonly GUIFont Font; + + public readonly Vector2 TextSize; + + public int TimesShown; + + public float Offset; + + public readonly Location RelatedLocation; + + public bool IsCurrentlyVisible; + + public MapNotification(string text, GUIFont font, List existingNotifications, Location relatedLocation) + { + Text = RichString.Rich(text); + Font = font; + TextSize = Font.MeasureString(Font.ForceUpperCase ? Text.SanitizedValue.ToUpper() : Text.SanitizedValue); + if (existingNotifications.Any()) + { + Offset = existingNotifications.Max(n => n.Offset + n.TextSize.X + GUI.IntScale(60)); + } + RelatedLocation = relatedLocation; + } + } + + private readonly List mapNotifications = new List(); + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) { - if (change.Messages.Any()) + var messages = change.GetMessages(location.Faction); + if (!messages.Any()) { return; } + + string msg = messages.GetRandom(Rand.RandSync.Unsynced) + .Replace("[previousname]", $"‖color:gui.yellow‖{prevName}‖end‖") + .Replace("[name]", $"‖color:gui.yellow‖{location.Name}‖end‖"); + location.LastTypeChangeMessage = msg; + + mapNotifications.Add(new MapNotification(msg, GUIStyle.SubHeadingFont, mapNotifications, location)); + } + + public void DrawNotifications(SpriteBatch spriteBatch, GUICustomComponent container) + { + Vector2 pos = new Vector2(container.Rect.Right, container.Rect.Center.Y); + foreach (var notification in mapNotifications) { - string msg = change.Messages[Rand.Range(0, change.Messages.Count)] - .Replace("[previousname]", $"‖color:gui.orange‖{prevName}‖end‖") - .Replace("[name]", $"‖color:gui.orange‖{location.Name}‖end‖"); - location.LastTypeChangeMessage = msg; - if (GameMain.Client != null) + Vector2 textPos = pos + new Vector2(notification.Offset, -notification.TextSize.Y / 2); + + notification.Font.DrawStringWithColors( + spriteBatch, + notification.Text.SanitizedValue, + textPos, + Color.White, 0.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0, + notification.Text.RichTextData); + + int margin = container.Rect.Width / 5; + notification.IsCurrentlyVisible = + textPos.X < container.Rect.Right - margin && + textPos.X + notification.TextSize.X > container.Rect.X + margin; + } + } + + private void UpdateNotifications(float deltaTime, GUICustomComponent mapContainer) + { + if (mapNotifications.Count < 5) + { + int maxIndex = 1; + while (TextManager.ContainsTag("randomnews" + maxIndex)) { - GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName").Value); + maxIndex++; } - else + string textTag = "randomnews" + Rand.Range(0, maxIndex); + if (TextManager.ContainsTag(textTag)) { - GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( - TextManager.Get("RadioAnnouncerName").Value, - msg, - Networking.ChatMessageType.Default, - sender: null); + mapNotifications.Add(new MapNotification(TextManager.Get(textTag).Value, GUIStyle.SubHeadingFont, mapNotifications, relatedLocation: null)); } - } + } + + for (int i = mapNotifications.Count - 1; i >= 0; i--) + { + var notification = mapNotifications[i]; + notification.Offset -= deltaTime * 75.0f; + if (notification.Offset < -notification.TextSize.X - mapContainer.Rect.Width) + { + notification.Offset = Math.Max(mapNotifications.Max(n => n.Offset + n.TextSize.X) + GUI.IntScale(60), 0); + notification.TimesShown++; + if (mapNotifications.Count > 5) + { + mapNotifications.RemoveAt(i); + } + else if (mapNotifications.Count > 3 && notification.TimesShown > 2) + { + mapNotifications.RemoveAt(i); + } + } + } + } + + private void CreateLocationInfoOverlay(Location location) + { + locationInfoOverlay = new GUIFrame(new RectTransform(new Point(GUI.IntScale(350), GUI.IntScale(350)), GUI.Canvas), style: "GUIToolTip") + { + UserData = location + }; + locationInfoOverlay.Color *= 0.8f; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), locationInfoOverlay.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + bool showReputation = hudVisibility > 0.0f && location.Type.HasOutpost && location.Reputation != null; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; + if (!location.Type.Name.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + } + + CreateSpacing(10); + + if (!location.Type.Description.IsNullOrEmpty()) + { + CreateTextWithIcon(location.Type.Description, location.Type.Sprite); + } + + int highestSubTier = location.HighestSubmarineTierAvailable(); + List<(SubmarineClass subClass, int tier)> overrideTiers = null; + if (location.CanHaveSubsForSale()) + { + overrideTiers = new List<(SubmarineClass subClass, int tier)>(); + foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) + { + if (subClass == SubmarineClass.Undefined) { continue; } + int highestClassTier = location.HighestSubmarineTierAvailable(subClass); + if (highestClassTier > 0 && highestClassTier > highestSubTier) + { + overrideTiers.Add((subClass, highestClassTier)); + } + } + } + if (highestSubTier > 0) + { + CreateTextWithIcon(TextManager.GetWithVariable("advancedsub.all", "[tiernumber]", highestSubTier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + if (overrideTiers != null) + { + foreach (var (subClass, tier) in overrideTiers) + { + CreateTextWithIcon(TextManager.GetWithVariable($"advancedsub.{subClass}", "[tiernumber]", tier.ToString()), icon: null, style: "LocationOverlaySubmarineIcon"); + } + } + + CreateSpacing(10); + + void CreateTextWithIcon(LocalizedString text, Sprite icon, string style = null) + { + var textHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, (int)GUIStyle.Font.MeasureString(text).Y), content.RectTransform), isHorizontal: true) + { + Stretch = true, + CanBeFocused = true + }; + var guiIcon = + style == null ? + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon) : + new GUIImage(new RectTransform(Vector2.One * 1.25f, textHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style); + var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1.0f), textHolder.RectTransform), text); + textBlock.RectTransform.MinSize = new Point((int)textBlock.TextSize.X, 0); + textHolder.RectTransform.MinSize = new Point((int)textBlock.TextSize.X + guiIcon.Rect.Width, 0); + } + + void CreateSpacing(int height) + { + new GUIFrame(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(height)), content.RectTransform), style: null); + } + + if (location.Faction != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), + RichString.Rich(TextManager.GetWithVariables("reputationgainnotification", + ("[value]", string.Empty), + ("[reputationname]", $"‖color:{XMLExtensions.ToStringHex(location.Faction.Prefab.IconColor)}‖{location.Faction.Prefab.Name}‖end‖")))) + { + Padding = Vector4.Zero + }; + + CreateSpacing(10); + + var repBarHolder = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, GUI.IntScale(25)), content.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), repBarHolder.RectTransform), onDraw: (sb, component) => + { + if (location.Reputation == null) { return; } + RoundSummary.DrawReputationBar(sb, component.Rect, location.Reputation.NormalizedValue); + }); + + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), repBarHolder.RectTransform), + location.Reputation.GetFormattedReputationText(), textAlignment: Alignment.CenterRight); + + new GUIImage(new RectTransform(new Vector2(0.25f, 0.5f), locationInfoOverlay.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.05f) }, + location.Faction.Prefab.Icon, scaleToFit: true) + { + Color = location.Faction.Prefab.IconColor * 0.5f + }; + CreateSpacing(20); + } + + locationInfoOverlay.RectTransform.NonScaledSize = + new Point( + Math.Max(locationInfoOverlay.Rect.Width, (int)(content.Children.Max(c => c is GUITextBlock textBlock ? textBlock.TextSize.X : c.RectTransform.MinSize.X) * 1.2f)), + (int)(content.Children.Sum(c => c.Rect.Height) / content.RectTransform.RelativeSize.Y)); } partial void ClearAnimQueue() @@ -280,12 +485,13 @@ namespace Barotrauma mapAnimQueue.Clear(); } - public void Update(float deltaTime, GUICustomComponent mapContainer) + public void Update(CampaignMode campaign, float deltaTime, GUICustomComponent mapContainer) { Rectangle rect = mapContainer.Rect; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + UpdateNotifications(deltaTime, mapContainer); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); if (currentDisplayLocation != null) { if (!currentDisplayLocation.Discovered) @@ -345,10 +551,39 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); Vector2 viewOffset = DrawOffset + drawOffsetNoise; + if (HighlightedLocation != null) + { + Vector2 highlightedLocationDrawPos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; + if (locationInfoOverlay == null || locationInfoOverlay.UserData != HighlightedLocation) + { + CreateLocationInfoOverlay(HighlightedLocation); + } + + Point offsetFromLocationIcon = new Point(GUI.IntScale(25)); + var locationInfoRt = locationInfoOverlay.RectTransform; + if (locationInfoRt.Pivot == Pivot.BottomLeft || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.Y = -offsetFromLocationIcon.Y; + } + if (locationInfoRt.Pivot == Pivot.TopRight || locationInfoRt.Pivot == Pivot.BottomRight) + { + offsetFromLocationIcon.X = -offsetFromLocationIcon.X; + } + locationInfoRt.ScreenSpaceOffset = highlightedLocationDrawPos.ToPoint() + offsetFromLocationIcon; + if (locationInfoOverlay.Rect.Bottom > rect.Bottom) + { + locationInfoRt.Pivot = Pivot.BottomLeft; + } + if (locationInfoOverlay.Rect.Right > rect.Right) + { + locationInfoRt.Pivot = locationInfoRt.Pivot == Pivot.TopLeft ? Pivot.TopRight : Pivot.BottomRight; + } + locationInfoOverlay?.AddToGUIUpdateList(order: 1); + } float closestDist = 0.0f; HighlightedLocation = null; - if (GUI.MouseOn == null || GUI.MouseOn == mapContainer) + if ((GUI.MouseOn == null || GUI.MouseOn == mapContainer)) { for (int i = 0; i < Locations.Count; i++) { @@ -374,7 +609,7 @@ namespace Barotrauma if (HighlightedLocation == null || dist < closestDist) { closestDist = dist; - HighlightedLocation = location; + HighlightedLocation = location; } } } @@ -453,12 +688,13 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); Discover(CurrentLocation); + Visit(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) { CurrentLocation.CreateStores(); - ProgressWorld(); + ProgressWorld(campaign); Radiation?.OnStep(1); } else @@ -467,12 +703,6 @@ namespace Barotrauma } } - if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) && PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation != null) - { - int distance = DistanceToClosestLocationWithOutpost(HighlightedLocation, out Location foundLocation); - DebugConsole.NewMessage($"Distance to closest outpost from {HighlightedLocation.Name} to {foundLocation?.Name} is {distance}"); - } - if (PlayerInput.PrimaryMouseButtonClicked() && HighlightedLocation == null) { SelectLocation(-1); @@ -481,10 +711,10 @@ namespace Barotrauma } } - public void Draw(SpriteBatch spriteBatch, GUICustomComponent mapContainer) + public void Draw(CampaignMode campaign, SpriteBatch spriteBatch, GUICustomComponent mapContainer) { tooltip = null; - var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); + var currentDisplayLocation = campaign?.GetCurrentDisplayLocation(); Rectangle rect = mapContainer.Rect; @@ -501,6 +731,8 @@ namespace Barotrauma Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + float missionIconScale = generationParams.MissionIcon != null ? 18.0f / generationParams.MissionIcon.SourceRect.Width : 1.0f; + Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); @@ -568,7 +800,9 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { Location location = Locations[i]; - if (IsInFogOfWar(location)) { continue; } + if (!location.Discovered && IsInFogOfWar(location)) { continue; } + bool isEndLocation = endLocations.Contains(location); + if (!GameMain.DebugDraw && isEndLocation && location != endLocations.First()) { continue; } Vector2 pos = rectCenter + (location.MapPosition + viewOffset) * zoom; Sprite locationSprite = location.IsCriticallyRadiated() ? location.Type.RadiationSprite ?? location.Type.Sprite : location.Type.Sprite; @@ -577,24 +811,54 @@ namespace Barotrauma drawRect.X = (int)pos.X - drawRect.Width / 2; drawRect.Y = (int)pos.Y - drawRect.Width / 2; + if (drawRect.X > rect.Right - GUI.IntScale(100) && generationParams.MissionIcon != null && location.AvailableMissions.Any()) + { + Vector2 offScreenMissionIconPos = new Vector2(rect.Right - GUI.IntScale(50), drawRect.Center.Y); + generationParams.MissionIcon.Draw(spriteBatch, + offScreenMissionIconPos, + generationParams.IndicatorColor, scale: missionIconScale * zoom); + GUI.Arrow.Draw(spriteBatch, + offScreenMissionIconPos + Vector2.UnitX * generationParams.MissionIcon.size.X * missionIconScale * zoom, + generationParams.IndicatorColor, MathHelper.PiOver2, scale: 0.5f); + } + + if (!rect.Intersects(drawRect)) { continue; } Color color = location.Type.SpriteColor; - if (!location.Discovered) { color = Color.White; } + if (!location.Visited) { color = Color.White; } if (location.Connections.Find(c => c.Locations.Contains(currentDisplayLocation)) == null) { color *= 0.5f; } float iconScale = location == currentDisplayLocation ? 1.2f : 1.0f; - if (location == HighlightedLocation) + if (location == HighlightedLocation) { iconScale *= 1.2f; } + if (isEndLocation) { iconScale *= 2.0f; } + + float notificationPulseAmount = 1.0f; + float notificationColorLerp = 0.0f; + if (mapNotifications.Any(n => n.RelatedLocation == location && n.IsCurrentlyVisible)) { - iconScale *= 1.2f; + float sin = MathF.Sin((float)Timing.TotalTime * 2.0f); + notificationPulseAmount = Math.Max(sin + 0.5f, 1.0f); + notificationColorLerp = (notificationPulseAmount - 1.0f) * 4.0f; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; } - locationSprite.Draw(spriteBatch, pos, color, + locationSprite.Draw(spriteBatch, pos, color, scale: generationParams.LocationIconSize / locationSprite.size.X * iconScale * zoom); + if (location.Faction != null) + { + float factionIconScale = iconScale * 0.7f; + Sprite factionIcon = location.Faction.Prefab.IconSmall ?? location.Faction.Prefab.Icon; + Color factionIconColor = Color.Lerp(color, location.Faction.Prefab.IconColor, notificationColorLerp); + factionIcon.Draw(spriteBatch, pos + new Vector2(-15, 15) * zoom, factionIconColor, + scale: generationParams.LocationIconSize / factionIcon.size.X * factionIconScale * zoom); + } + if (location == currentDisplayLocation) { if (SelectedLocation != null) @@ -626,7 +890,10 @@ namespace Barotrauma { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; - generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUIStyle.Red, scale: typeChangeIconScale * zoom); + Color iconColor = GUIStyle.Red; + color = Color.Lerp(color, GUIStyle.Yellow, notificationColorLerp); + iconScale *= notificationPulseAmount; + generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, iconColor, scale: typeChangeIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { @@ -635,14 +902,17 @@ namespace Barotrauma } if (location != CurrentLocation && generationParams.MissionIcon != null) { - if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || location.AvailableMissions.Any(m => m.Prefab.Type == MissionType.GoTo)) + if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || + location.AvailableMissions.Any(m => m.Locations[0] == m.Locations[1])) { Vector2 missionIconPos = pos + new Vector2(1.35f, 0.35f) * generationParams.LocationIconSize * 0.5f * zoom; - float missionIconScale = 18.0f / generationParams.MissionIcon.SourceRect.Width; generationParams.MissionIcon.Draw(spriteBatch, missionIconPos, generationParams.IndicatorColor, scale: missionIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, missionIconPos) < generationParams.MissionIcon.SourceRect.Width * zoom && IsPreferredTooltip(missionIconPos)) { - var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)).Concat(location.AvailableMissions.Where(m => m.Prefab.Type == MissionType.GoTo)).Distinct(); + var availableMissions = CurrentLocation.AvailableMissions + .Where(m => m.Locations.Contains(location)) + .Concat(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])) + .Distinct(); tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); } } @@ -651,23 +921,19 @@ namespace Barotrauma if (GameMain.DebugDraw) { Vector2 dPos = pos; - if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) + if (location == HighlightedLocation) { + dPos.Y -= 80; + GUI.DrawString(spriteBatch, dPos + new Vector2(15, 32), "Faction: " + (location.Faction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, dPos + new Vector2(15, 50), "Secondary Faction: " + (location.SecondaryFaction?.Prefab.Name ?? "none"), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); dPos.Y += 48; - string name = $"Reputation: {location.Name}"; - Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); - dPos.Y += nameSize.Y + 16; - Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); - bgRect.Inflate(8,8); - Color barColor = ToolBox.GradientLerp(location.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); - GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); - string reputationValue = ((int)location.Reputation.Value).ToString(); - Vector2 repValueSize = GUIStyle.SubHeadingFont.MeasureString(reputationValue); - GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); + if (PlayerInput.KeyDown(Keys.LeftShift)) + { + GUI.DrawString(spriteBatch, new Vector2(150,150), "Dist: " + + GetDistanceToClosestLocationOrConnection(CurrentLocation, int.MaxValue, loc => loc == location), Color.White, Color.Black, font: GUIStyle.SubHeadingFont); + + } } dPos.Y += 48; GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); @@ -684,97 +950,6 @@ namespace Barotrauma GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } - else if (HighlightedLocation != null) - { - drawRadiationTooltip = false; - Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; - pos.X += 50 * zoom; - pos.X = (int)pos.X; - pos.Y = (int)pos.Y; - Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); - Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); - Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); - - int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); - List<(SubmarineClass subClass, int tier)> overrideTiers = null; - if (HighlightedLocation.CanHaveSubsForSale()) - { - overrideTiers = new List<(SubmarineClass subClass, int tier)>(); - foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) - { - if (subClass == SubmarineClass.Undefined) { continue; } - int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); - if (highestClassTier > 0 && highestClassTier > highestSubTier) - { - overrideTiers.Add((subClass, highestClassTier)); - } - } - } - int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0); - size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; - - bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - LocalizedString repLabelText = null, repValueText = null; - Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; - if (showReputation) - { - repLabelText = TextManager.Get("reputation"); - repLabelSize = GUIStyle.Font.MeasureString(repLabelText); - repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); - size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; - repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); - } - - GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, - new Rectangle( - (int)(pos.X - 60 * GUI.Scale), - (int)(pos.Y - size.Y), - (int)(size.X + 120 * GUI.Scale), - (int)(size.Y * 2.2f)), - Color.Black * hudVisibility); - - var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); - topLeftPos += new Vector2(0.0f, nameSize.Y); - DrawText(HighlightedLocation.Type.Name); - if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) - { - topLeftPos += new Vector2(0.0f, descSize.Y); - DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); - } - - if (highestSubTier > 0) - { - DrawSubAvailabilityText("advancedsub.all", highestSubTier); - } - if (overrideTiers != null) - { - foreach (var (subClass, tier) in overrideTiers) - { - DrawSubAvailabilityText($"advancedsub.{subClass}", tier); - } - } - void DrawSubAvailabilityText(string tag, int tier) - { - topLeftPos += new Vector2(0.0f, typeSize.Y); - DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); - } - - if (showReputation) - { - topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - DrawText(repLabelText.Value); - topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); - Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); - RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); - } - - void DrawText(LocalizedString text, GUIFont font = null) => GUI.DrawString(spriteBatch, topLeftPos, text, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: font); - } if (drawRadiationTooltip) { @@ -892,7 +1067,7 @@ namespace Barotrauma } float a = 1.0f; - if (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered) + if (!connection.Locations[0].Visited && !connection.Locations[1].Visited) { if (IsInFogOfWar(connection.Locations[0])) { @@ -961,17 +1136,15 @@ namespace Barotrauma if (connection.Locked) { var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; - var unlockEvent = - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); + var unlockEvent = EventPrefab.GetUnlockPathEvent(gateLocation.LevelData.Biome.Identifier, gateLocation.Faction); if (unlockEvent != null) { Reputation unlockReputation = CurrentLocation.Reputation; Faction unlockFaction = null; - if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) + if (!unlockEvent.Faction.IsEmpty) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.Faction); unlockReputation = unlockFaction?.Reputation; } @@ -1042,13 +1215,14 @@ namespace Barotrauma private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect) { generationParams.DecorativeGraphSprite.Draw(spriteBatch, (int)((Timing.TotalTime * 5.0f) % generationParams.DecorativeGraphSprite.FrameCount), - new Vector2(rect.Left, rect.Top), Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale); + new Vector2(rect.X, rect.Bottom - (generationParams.DecorativeGraphSprite.FrameSize.Y + 30) * GUI.Scale), + Color.White, Vector2.Zero, 0, Vector2.One * GUI.Scale, SpriteEffects.FlipVertically); GUI.DrawString(spriteBatch, new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)), "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, - new Vector2(rect.X + GUI.IntScale(15), rect.Bottom - GUI.IntScale(25)), + new Vector2(rect.X + GUI.IntScale(5), rect.Y + GUI.IntScale(5)), "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 51fa11094..151e15a14 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -36,7 +36,7 @@ namespace Barotrauma public static List CopiedList = new List(); - private static List highlightedList = new List(); + private static List highlightedInEditorList = new List(); private static float highlightTimer; @@ -99,11 +99,24 @@ namespace Barotrauma { 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; + + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.000001f + + ID % 100 * 0.0000001f; return Math.Min(depth, 1.0f); } + protected Vector2 GetCollapseEffectOffset() + { + if (Level.Loaded?.Renderer?.CollapseEffectStrength is float collapseEffectStrength and > 0.0f && Submarine is not { Info.Type: SubmarineType.Player }) + { + Vector2 noisePos = new Vector2( + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.5f) - 0.5f, + (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.1f) - 0.5f); + Vector2 offsetFromOrigin = Level.Loaded.Renderer.CollapseEffectOrigin - DrawPosition; + return offsetFromOrigin * MathF.Pow(collapseEffectStrength, MathHelper.Lerp(1, 4, ID % 1000 / 1000.0f)) + (noisePos * 100.0f * collapseEffectStrength); + } + return Vector2.Zero; + } + /// /// Update the selection logic in submarine editor /// @@ -118,10 +131,7 @@ namespace Barotrauma return; } - foreach (MapEntity e in mapEntityList) - { - e.isHighlighted = false; - } + ClearHighlightedEntities(); if (DisableSelect) { @@ -249,11 +259,10 @@ namespace Barotrauma if (i == 0) highLightedEntity = e; } } - UpdateHighlighting(highlightedEntities); } - if (highLightedEntity != null) highLightedEntity.isHighlighted = true; + if (highLightedEntity != null) { highLightedEntity.IsHighlighted = true; } } if (GUI.KeyboardDispatcher.Subscriber == null) @@ -275,7 +284,6 @@ namespace Barotrauma if (startMovingPos != Vector2.Zero) { Item targetContainer = GetPotentialContainer(position, SelectedList); - if (targetContainer != null) { targetContainer.IsHighlighted = true; } if (PlayerInput.PrimaryMouseButtonReleased()) @@ -597,10 +605,10 @@ namespace Barotrauma if (highlightedListBox != null) { if (GUI.MouseOn == highlightedListBox || highlightedListBox.IsParentOf(GUI.MouseOn)) return; - if (highlightedEntities.SequenceEqual(highlightedList)) return; + if (highlightedEntities.SequenceEqual(highlightedInEditorList)) return; } - highlightedList = highlightedEntities; + highlightedInEditorList = highlightedEntities; highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { @@ -1083,7 +1091,7 @@ namespace Barotrauma private void UpdateResizing(Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; @@ -1184,7 +1192,7 @@ namespace Barotrauma private void DrawResizing(SpriteBatch spriteBatch, Camera cam) { - isHighlighted = true; + IsHighlighted = true; int startX = ResizeHorizontal ? -1 : 0; int StartY = ResizeVertical ? -1 : 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b41a0f0a6..e881ebf77 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -226,6 +226,9 @@ namespace Barotrauma min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); } + Vector2 offset = GetCollapseEffectOffset(); + min += offset; + max += offset; if (min.X > worldView.Right || max.X < worldView.X) { return false; } if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } @@ -295,6 +298,7 @@ namespace Barotrauma if (isWiringMode) { color *= 0.15f; } Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; + drawOffset += GetCollapseEffectOffset(); float depth = GetDrawDepth(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 06cfbc539..614365566 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -35,6 +35,13 @@ namespace Barotrauma Rectangle camView = cam.WorldView; camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2); + if (Level.Loaded?.Renderer?.CollapseEffectStrength is > 0.0f) + { + //force everything to be visible when the collapse effect (which moves everything to a single point) is active + camView = Rectangle.Union(AbsRect(camView.Location.ToVector2(), camView.Size.ToVector2()), new Rectangle(Point.Zero, Level.Loaded.Size)); + camView.Y += camView.Height; + } + if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold && Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold && Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index b3f830140..8491dd736 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -116,7 +116,7 @@ namespace Barotrauma bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && - (PlayerInput.MidButtonHeld() || PlayerInput.LeftButtonHeld())) + (PlayerInput.MidButtonHeld() || PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; moveSpeed.X = -moveSpeed.X; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 1d862e8a6..cded39e96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -39,7 +39,7 @@ namespace Barotrauma { Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } - if (isObstructed) + if (!IsTraversable) { clr = Color.Black; } @@ -84,7 +84,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); + (IsTraversable ? GUIStyle.Green : Color.Gray) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { @@ -123,6 +123,11 @@ namespace Barotrauma } } } + else if (spawnType == SpawnType.ExitPoint && ExitPointSize != Point.Zero) + { + GUI.DrawRectangle(spriteBatch, drawPos - ExitPointSize.ToVector2() / 2, ExitPointSize.ToVector2(), Color.Cyan, thickness: 5); + } + GUIStyle.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), @@ -170,9 +175,9 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.Space)) { - foreach (MapEntity e in mapEntityList) + foreach (MapEntity e in HighlightedEntities) { - if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } + if (e is not WayPoint || e == this) { continue; } if (linkedTo.Contains(e)) { @@ -251,6 +256,7 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { + var prevSpawnType = spawnType; GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; var values = (SpawnType[])Enum.GetValues(typeof(SpawnType)); int currIndex = values.IndexOf(spawnType); @@ -267,6 +273,7 @@ namespace Barotrauma } spawnType = values[currIndex]; spawnTypeText.Text = spawnType.ToString(); + if (spawnType == SpawnType.ExitPoint || prevSpawnType == SpawnType.ExitPoint) { CreateEditingHUD(); } return true; } @@ -412,6 +419,28 @@ namespace Barotrauma textBox.Text = string.Join(",", tags); textBox.Flash(GUIStyle.Green); }; + + if (SpawnType == SpawnType.ExitPoint) + { + var sizeField = GUI.CreatePointField(ExitPointSize, GUI.IntScale(20), TextManager.Get("dimensions"), paddedFrame.RectTransform); + GUINumberInput xField = null, yField = null; + foreach (GUIComponent child in sizeField.GetAllChildren()) + { + if (yField == null) + { + yField = child as GUINumberInput; + } + else + { + xField = child as GUINumberInput; + if (xField != null) { break; } + } + } + xField.MinValueInt = 0; + xField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(numberInput.IntValue, ExitPointSize.Y); }; + yField.MinValueInt = 0; + yField.OnValueChanged = (numberInput) => { ExitPointSize = new Point(ExitPointSize.X, numberInput.IntValue); }; + } } editingHUD.RectTransform.Resize(new Point( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 1809817cb..f86c9cd48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -332,7 +332,6 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -414,7 +413,6 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index dc8dae70b..5324bf1b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -326,7 +326,7 @@ namespace Barotrauma.Networking return serverEndpoint switch { LidgrenEndpoint lidgrenEndpoint => new LidgrenClientPeer(lidgrenEndpoint, callbacks, ownerKey), - SteamP2PEndpoint _ when ownerKey is Some { Value: var key } => new SteamP2POwnerPeer(callbacks, key), + SteamP2PEndpoint _ when ownerKey.TryUnwrap(out var key) => new SteamP2POwnerPeer(callbacks, key), SteamP2PEndpoint steamP2PServerEndpoint when ownerKey.IsNone() => new SteamP2PClientPeer(steamP2PServerEndpoint, callbacks), _ => throw new ArgumentOutOfRangeException() }; @@ -990,7 +990,7 @@ namespace Barotrauma.Networking GameMain.ModDownloadScreen.Reset(); ContentPackageManager.EnabledPackages.Restore(); - CampaignMode.StartRoundCancellationToken?.Cancel(); + GameMain.GameSession?.Campaign?.CancelStartRound(); if (SteamManager.IsInitialized) { @@ -2627,7 +2627,13 @@ namespace Barotrauma.Networking using (var segmentTable = SegmentTableWriter.StartWriting(msg)) { segmentTable.StartNewSegment(ClientNetSegment.Vote); - Voting.ClientWrite(msg, voteType, data); + bool succeeded = Voting.ClientWrite(msg, voteType, data); + if (!succeeded) + { + throw new Exception( + $"Failed to write vote of type {voteType}: " + + $"data was of invalid type {data?.GetType().Name ?? "NULL"}"); + } } ClientPeer.Send(msg, DeliveryMethod.Reliable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 54c4d7ca2..5c77d37c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Networking var packet = INetSerializableStruct.Read(inc); - packet.SteamAuthTicket.TryUnwrap(out byte[] ticket); + packet.SteamAuthTicket.TryUnwrap(out var ticket); Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId); if (authSessionStartState != Steamworks.BeginAuthResult.OK) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index adbf863df..0c778b22c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -1,5 +1,6 @@ #nullable enable +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -69,6 +70,9 @@ namespace Barotrauma.Networking [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } public Version GameVersion { get; set; } = new Version(0, 0, 0, 0); @@ -281,7 +285,7 @@ namespace Barotrauma.Networking // ----------------------------------------------------------------------------- - float elementHeight = 0.075f; + const float elementHeight = 0.075f; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); @@ -294,6 +298,11 @@ namespace Barotrauma.Networking serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); }; msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); }; + var languageLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("Language")); + new GUITextBlock(new RectTransform(Vector2.One, languageLabel.RectTransform), + ServerLanguageOptions.Options.FirstOrNull(o => o.Identifier == Language)?.Label ?? TextManager.Get("Unknown"), + textAlignment: Alignment.Right); + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), @@ -363,7 +372,7 @@ namespace Barotrauma.Networking packageText.Selected = true; } //workshop download link found - else if (package.Id is Some { Value: var ugcId } && ugcId is SteamWorkshopId) + else if (package.Id.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId) { packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name); } @@ -417,6 +426,7 @@ namespace Barotrauma.Networking GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty; if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } + Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c6a97de44..339e6051b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Barotrauma.Steam; namespace Barotrauma.Networking { @@ -78,7 +79,7 @@ namespace Barotrauma.Networking } } private Dictionary tempMonsterEnabled; - + partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); @@ -367,6 +368,15 @@ namespace Barotrauma.Networking //*********************************************** + // Language + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); + var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDD.AddItem(language.Label, language.Identifier); + } + GetPropertyData(nameof(Language)).AssignGUIComponent(languageDD); + //changing server visibility on the fly is not supported in dedicated servers if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index bf262be14..d23386f33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -116,7 +116,9 @@ namespace Barotrauma.Networking bool spectating = Character.Controlled == null; float rangeMultiplier = spectating ? 2.0f : 1.0f; WifiComponent radio = null; - var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; + var messageType = + !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? + ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 67ba366d8..466b9418b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -13,13 +13,11 @@ namespace Barotrauma { public SubmarineInfo SubmarineInfo { get; set; } public bool TransferItems { get; set; } - public int DeliveryFee { get; set; } - public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems, int deliveryFee) + public SubmarineVoteInfo(SubmarineInfo submarineInfo, bool transferItems) { SubmarineInfo = submarineInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; } } @@ -128,64 +126,72 @@ namespace Barotrauma UpdateVoteTexts(connectedClients, VoteType.Sub); } - public void ClientWrite(IWriteMessage msg, VoteType voteType, object data) + /// + /// Returns true if the given data is valid for the given vote type, + /// returns false otherwise. If it returns false, the message must + /// be discarded or reset by the caller, as it is now malformed :) + /// + public bool ClientWrite(IWriteMessage msg, VoteType voteType, object data) { msg.WriteByte((byte)voteType); switch (voteType) { case VoteType.Sub: - if (!(data is SubmarineInfo sub)) { return; } + if (data is not SubmarineInfo sub) { return false; } msg.WriteInt32(sub.EqualityCheckVal); - if (sub.EqualityCheckVal == 0) + if (sub.EqualityCheckVal <= 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for msg.WriteString(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: - if (!(data is GameModePreset gameMode)) { return; } + if (data is not GameModePreset gameMode) { return false; } msg.WriteIdentifier(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool endRound) { return false; } + msg.WriteBoolean(endRound); break; case VoteType.Kick: - if (!(data is Client votedClient)) { return; } + if (data is not Client votedClient) { return false; } msg.WriteByte(votedClient.SessionId); break; case VoteType.StartRound: - if (!(data is bool)) { return; } - msg.WriteBoolean((bool)data); + if (data is not bool startRound) { return false; } + msg.WriteBoolean(startRound); break; case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (data is (SubmarineInfo voteSub, bool transferItems)) - { - //initiate sub vote - msg.WriteBoolean(true); - msg.WriteString(voteSub.Name); - msg.WriteBoolean(transferItems); - } - else + switch (data) { - // vote - if (!(data is int)) { return; } - msg.WriteBoolean(false); - msg.WriteInt32((int)data); + case (SubmarineInfo voteSub, bool transferItems): + //initiate sub vote + msg.WriteBoolean(true); + msg.WriteString(voteSub.Name); + msg.WriteBoolean(transferItems); + break; + case int vote: + // vote + msg.WriteBoolean(false); + msg.WriteInt32(vote); + break; + default: + return false; } break; case VoteType.TransferMoney: - if (!(data is int)) { return; } + if (data is not int money) { return false; } msg.WriteBoolean(false); //not initiating a vote - msg.WriteInt32((int)data); + msg.WriteInt32(money); break; } msg.WritePadBits(); + return true; } public void ClientRead(IReadMessage inc) @@ -322,33 +328,34 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.SwitchSub: string subName2 = inc.ReadString(); - var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); bool transferItems = inc.ReadBoolean(); - int deliveryFee = inc.ReadInt16(); - if (submarineInfo == null) + if (GameMain.GameSession != null) { - DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); - return; + var submarineInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName2) ?? GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); + if (submarineInfo == null) + { + DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); + return; + } + submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems); } - submarineVoteInfo = new SubmarineVoteInfo(submarineInfo, transferItems, deliveryFee); break; } - GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); - + GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); if (passed && submarineVoteInfo.SubmarineInfo is { } subInfo) { switch (voteType) { case VoteType.PurchaseAndSwitchSub: GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, 0); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; case VoteType.PurchaseSub: GameMain.GameSession.PurchaseSubmarine(subInfo); break; case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems, submarineVoteInfo.DeliveryFee); + GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 468f07e54..55d0d15cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -85,6 +85,9 @@ namespace Barotrauma.Particles [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] public bool CopyEntityDir { get; set; } + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Only relevant for status effects. Makes the emitter copy the angle from the target of the effect instead of the entity applying the effect.")] + public bool CopyTargetAngle { get; set; } + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } @@ -203,7 +206,7 @@ namespace Barotrauma.Particles position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); } - var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, lifeTimeMultiplier: Prefab.Properties.LifeTimeMultiplier, tracerPoints: tracerPoints); if (particle != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index dfdd7342a..9aa60c592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -75,21 +75,21 @@ namespace Barotrauma.Particles return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer, tracerPoints:tracerPoints); } - public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, float lifeTimeMultiplier = 1f, Tuple tracerPoints = null) { if (prefab == null || prefab.Sprites.Count == 0) { return null; } - if (particleCount >= MaxParticles) { for (int i = 0; i < particleCount; i++) { - if (particles[i].Prefab.Priority < prefab.Priority) + if (particles[i].Prefab.Priority < prefab.Priority || + (!particles[i].Prefab.DrawAlways && prefab.DrawAlways)) { RemoveParticle(i); break; } } - if (particleCount >= MaxParticles) { return null; } + if (particleCount >= MaxParticles) { return null; } } Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); @@ -109,26 +109,30 @@ namespace Barotrauma.Particles Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } - if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + if (!prefab.DrawAlways) + { + if (minPos.X > expandedViewRect.Right || maxPos.X < expandedViewRect.X) { return null; } + if (minPos.Y > expandedViewRect.Y || maxPos.Y < expandedViewRect.Y - expandedViewRect.Height) { return null; } + } if (particles[particleCount] == null) { particles[particleCount] = new Particle(); } - particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, prefab.DrawOnTop, collisionIgnoreTimer, lifeTimeMultiplier, tracerPoints: tracerPoints); particleCount++; return particles[particleCount - 1]; } - public List GetPrefabList() + public static List GetPrefabList() { return ParticlePrefab.Prefabs.ToList(); } - public ParticlePrefab FindPrefab(string prefabName) + public static ParticlePrefab FindPrefab(string prefabName) { - return ParticlePrefab.Prefabs.Find(p => p.Identifier == prefabName); + ParticlePrefab.Prefabs.TryGet(prefabName, out ParticlePrefab prefab); + return prefab; } private void RemoveParticle(int index) @@ -170,7 +174,7 @@ namespace Barotrauma.Particles remove = true; } - if (remove) RemoveParticle(i); + if (remove) { RemoveParticle(i); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index e758564c5..0b7aa21af 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -185,6 +185,9 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] public bool DrawOnTop { get; private set; } + [Editable, Serialize(false, IsPropertySaveable.No, description: "Draw the particle even when it's calculated to be outside of view (the formula doesn't take scales into account). ")] + public bool DrawAlways { get; private set; } + [Editable, Serialize(ParticleBlendState.AlphaBlend, IsPropertySaveable.No, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index a0ac4f61a..628b582c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -79,13 +79,13 @@ namespace Barotrauma new Vector2(DrawPosition.X, -DrawPosition.Y), Color.Cyan, 0, 5); } - if (bodyShapeTexture == null && IsValidShape(radius, height, width)) + if (bodyShapeTexture == null && IsValidShape(Radius, Height, Width)) { switch (BodyShape) { case Shape.Rectangle: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(width), ConvertUnits.ToDisplayUnits(height)); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Width), ConvertUnits.ToDisplayUnits(Height)); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -96,14 +96,14 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateRectangle( - (int)ConvertUnits.ToDisplayUnits(width * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(height * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Width * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Height * bodyShapeTextureScale)); break; } case Shape.Capsule: case Shape.HorizontalCapsule: { - float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(radius), ConvertUnits.ToDisplayUnits(Math.Max(height, width))); + float maxSize = Math.Max(ConvertUnits.ToDisplayUnits(Radius), ConvertUnits.ToDisplayUnits(Math.Max(Height, Width))); if (maxSize > 128.0f) { bodyShapeTextureScale = 128.0f / maxSize; @@ -114,20 +114,20 @@ namespace Barotrauma } bodyShapeTexture = GUI.CreateCapsule( - (int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale), - (int)ConvertUnits.ToDisplayUnits(Math.Max(height, width) * bodyShapeTextureScale)); + (int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale), + (int)ConvertUnits.ToDisplayUnits(Math.Max(Height, Width) * bodyShapeTextureScale)); break; } case Shape.Circle: - if (ConvertUnits.ToDisplayUnits(radius) > 128.0f) + if (ConvertUnits.ToDisplayUnits(Radius) > 128.0f) { - bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(radius); + bodyShapeTextureScale = 128.0f / ConvertUnits.ToDisplayUnits(Radius); } else { bodyShapeTextureScale = 1.0f; } - bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(radius * bodyShapeTextureScale)); + bodyShapeTexture = GUI.CreateCircle((int)ConvertUnits.ToDisplayUnits(Radius * bodyShapeTextureScale)); break; default: throw new NotImplementedException(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index efd20b32b..2c24dd4da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -11,15 +11,13 @@ namespace Barotrauma public enum MouseButton { None = -1, - LeftMouse = 0, - RightMouse = 1, + PrimaryMouse = 0, + SecondaryMouse = 1, MiddleMouse = 2, MouseButton4 = 3, MouseButton5 = 4, MouseWheelUp = 5, - MouseWheelDown = 6, - PrimaryMouse, - SecondaryMouse + MouseWheelDown = 6 } public class KeyOrMouse @@ -65,10 +63,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonHeld(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonHeld(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonHeld(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonHeld(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonHeld(); case MouseButton.MouseButton4: @@ -95,10 +89,6 @@ namespace Barotrauma return PlayerInput.PrimaryMouseButtonClicked(); case MouseButton.SecondaryMouse: return PlayerInput.SecondaryMouseButtonClicked(); - case MouseButton.LeftMouse: - return PlayerInput.LeftButtonClicked(); - case MouseButton.RightMouse: - return PlayerInput.RightButtonClicked(); case MouseButton.MiddleMouse: return PlayerInput.MidButtonClicked(); case MouseButton.MouseButton4: @@ -218,11 +208,11 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + return PlayerInput.PrimaryMouseLabel; case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + return PlayerInput.SecondaryMouseLabel; default: - return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + return TextManager.Get($"Input.{MouseButton}"); } } else @@ -270,6 +260,9 @@ namespace Barotrauma } #endif + public static readonly LocalizedString PrimaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Left" : "Right")}Mouse"); + public static readonly LocalizedString SecondaryMouseLabel = TextManager.Get($"Input.{(!MouseButtonsSwapped() ? "Right" : "Left")}Mouse"); + public static Vector2 MousePosition { get { return new Vector2(mouseState.Position.X, mouseState.Position.Y); } @@ -317,120 +310,48 @@ namespace Barotrauma } public static bool PrimaryMouseButtonHeld() - { - if (MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool PrimaryMouseButtonDown() - { - if (MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool PrimaryMouseButtonReleased() - { - if (MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool PrimaryMouseButtonClicked() - { - if (MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool SecondaryMouseButtonHeld() - { - if (!MouseButtonsSwapped()) - { - return RightButtonHeld(); - } - return LeftButtonHeld(); - } - - public static bool SecondaryMouseButtonDown() - { - if (!MouseButtonsSwapped()) - { - return RightButtonDown(); - } - return LeftButtonDown(); - } - - public static bool SecondaryMouseButtonReleased() - { - if (!MouseButtonsSwapped()) - { - return RightButtonReleased(); - } - return LeftButtonReleased(); - } - - public static bool SecondaryMouseButtonClicked() - { - if (!MouseButtonsSwapped()) - { - return RightButtonClicked(); - } - return LeftButtonClicked(); - } - - public static bool LeftButtonHeld() { return AllowInput && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonDown() + public static bool PrimaryMouseButtonDown() { return AllowInput && oldMouseState.LeftButton == ButtonState.Released && mouseState.LeftButton == ButtonState.Pressed; } - public static bool LeftButtonReleased() + public static bool PrimaryMouseButtonReleased() { return AllowInput && mouseState.LeftButton == ButtonState.Released; } - public static bool LeftButtonClicked() + public static bool PrimaryMouseButtonClicked() { return (AllowInput && oldMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released); } - public static bool RightButtonHeld() + public static bool SecondaryMouseButtonHeld() { return AllowInput && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonDown() + public static bool SecondaryMouseButtonDown() { return AllowInput && oldMouseState.RightButton == ButtonState.Released && mouseState.RightButton == ButtonState.Pressed; } - public static bool RightButtonReleased() + public static bool SecondaryMouseButtonReleased() { return AllowInput && mouseState.RightButton == ButtonState.Released; } - public static bool RightButtonClicked() + public static bool SecondaryMouseButtonClicked() { return (AllowInput && oldMouseState.RightButton == ButtonState.Pressed diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index 17e268d1e..fc8859439 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -7,17 +7,13 @@ namespace Barotrauma { class CampaignEndScreen : Screen { - private Video video; - private readonly CreditsPlayer creditsPlayer; private readonly Camera cam; public Action OnFinished; - private LocalizedString textOverlay; - private float textOverlayTimer; - private Vector2 textOverlaySize; + protected SlideshowPlayer slideshowPlayer; public CampaignEndScreen() { @@ -27,43 +23,45 @@ namespace Barotrauma ScrollBarEnabled = false, AllowMouseWheelScroll = false }; - new GUIButton(new RectTransform(new Vector2(0.1f), creditsPlayer.RectTransform, Anchor.BottomRight, maxSize: new Point(300, 50)) { AbsoluteOffset = new Point(GUI.IntScale(20)) }, - TextManager.Get("close")) + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => - { - creditsPlayer.Scroll = 1.0f; - return true; - } + creditsPlayer.Scroll = 1.0f; + return true; }; + cam = new Camera(); } public override void Select() { base.Select(); - - textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUIStyle.Font); - textOverlaySize = GUIStyle.Font.MeasureString(textOverlay); - textOverlayTimer = 0.0f; - - video = Video.Load(GameMain.GraphicsDeviceManager.GraphicsDevice, GameMain.SoundManager, "Content/SplashScreens/Ending.webm"); - video.Play(); + if (SlideshowPrefab.Prefabs.TryGet("campaignending".ToIdentifier(), out var slideshow)) + { + slideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); + } creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); + UnlockAchievement("campaigncompleted"); + UnlockAchievement( + GameMain.GameSession is { Campaign.Settings.RadiationEnabled: true } ? + "campaigncompleted_radiationenabled" : + "campaigncompleted_radiationdisabled"); + + static void UnlockAchievement(string id) + { + SteamAchievementManager.UnlockAchievement(id.ToIdentifier(), unlockClients: true); + } } public override void Deselect() { - video?.Dispose(); - video = null; GUI.HideCursor = false; SoundPlayer.OverrideMusicType = Identifier.Empty; } public override void Update(double deltaTime) { + slideshowPlayer?.UpdateManually((float)deltaTime); if (creditsPlayer.Finished) { OnFinished?.Invoke(); @@ -73,46 +71,18 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - spriteBatch.Begin(); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); graphics.Clear(Color.Black); - if (video.IsPlaying) + SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); + if (slideshowPlayer != null && !slideshowPlayer.Finished) { - GUI.HideCursor = !GUI.PauseMenuOpen; - spriteBatch.Draw(video.GetTexture(), new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); + slideshowPlayer.DrawManually(spriteBatch); } else { - SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); - float duration = 20.0f; - float creditsDelay = 3.0f; - if (textOverlayTimer < duration + creditsDelay) - { - float textAlpha; - float fadeInTime = 5.0f, fadeOutTime = 3.0f; - textOverlayTimer += (float)deltaTime; - if (textOverlayTimer < fadeInTime) - { - textAlpha = textOverlayTimer / fadeInTime; - } - else if (textOverlayTimer > duration - fadeOutTime) - { - textAlpha = Math.Min((duration - textOverlayTimer) / fadeOutTime, 1.0f); - } - else - { - textAlpha = 1.0f; - } - GUIStyle.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); - } - else - { - GUI.HideCursor = false; - creditsPlayer.Visible = true; - } + GUI.HideCursor = false; + creditsPlayer.Visible = true; } - spriteBatch.End(); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index 0bc8adcd3..bb8f98570 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -38,7 +38,6 @@ namespace Barotrauma protected set; } - public CampaignSettings CurrentSettings = new CampaignSettings(element: null); public GUIButton CampaignCustomizeButton { get; set; } public GUIMessageBox CampaignCustomizeSettings { get; set; } @@ -124,6 +123,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue SelectedPreset; public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; @@ -135,6 +135,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + PresetName = SelectedPreset.GetValue(), TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), @@ -185,9 +186,13 @@ namespace Barotrauma { const float verticalSize = 0.14f; + bool loadingPreset = false; + GUILayoutGroup presetDropdownLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, verticalSize), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), TextManager.Get("campaignsettingpreset")); - GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length); + GUIDropDown presetDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), presetDropdownLayout.RectTransform), elementCount: CampaignModePresets.List.Length + 1); + presetDropdown.AddItem(TextManager.Get("karmapreset.custom"), null); + presetDropdown.Select(0); presetDropdownLayout.RectTransform.MinSize = new Point(0, presetDropdown.Rect.Height); @@ -195,21 +200,30 @@ namespace Barotrauma { string name = settings.PresetName; presetDropdown.AddItem(TextManager.Get($"preset.{name}").Fallback(name), settings); + + if (settings.PresetName.Equals(prevSettings.PresetName, StringComparison.OrdinalIgnoreCase)) + { + presetDropdown.SelectItem(settings); + } } + var presetValue = new SettingValue( + get: () => presetDropdown.SelectedData is CampaignSettings settings ? settings.PresetName : string.Empty, + set: static _ => { }); // we do not need a way to set this value + GUIListBox settingsList = new GUIListBox(new RectTransform(new Vector2(1f, 1f - verticalSize), parent.RectTransform)) { Spacing = GUI.IntScale(5) }; SettingValue tutorialEnabled = isSinglePlayer ? - CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize) : - new SettingValue(() => false, b => { }); - SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize); + CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableTutorial"), TextManager.Get("campaignoption.enabletutorial.tooltip"), prevSettings.TutorialEnabled, verticalSize, OnValuesChanged) : + new SettingValue(static () => false, static _ => { }); + SettingValue radiationEnabled = CreateTickbox(settingsList.Content, TextManager.Get("CampaignOption.EnableRadiation"), TextManager.Get("campaignoption.enableradiation.tooltip"), prevSettings.RadiationEnabled, verticalSize, OnValuesChanged); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); SettingCarouselElement prevStartingSet = startingSetOptions.FirstOrNull(element => element.Value == prevSettings.StartItemSet) ?? startingSetOptions[1]; - SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions); + SettingValue startingSetInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startitemset"), TextManager.Get("startitemsettooltip"), prevStartingSet, verticalSize, startingSetOptions, OnValuesChanged); ImmutableArray> fundOptions = ImmutableArray.Create( new SettingCarouselElement(StartingBalanceAmount.Low, "startingfunds.low"), @@ -218,7 +232,7 @@ namespace Barotrauma ); SettingCarouselElement prevStartingFund = fundOptions.FirstOrNull(element => element.Value == prevSettings.StartingBalanceAmount) ?? fundOptions[1]; - SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions); + SettingValue startingFundsInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("startingfundsdescription"), TextManager.Get("startingfundstooltip"), prevStartingFund, verticalSize, fundOptions, OnValuesChanged); ImmutableArray> difficultyOptions = ImmutableArray.Create( new SettingCarouselElement(GameDifficulty.Easy, "difficulty.easy"), @@ -228,30 +242,38 @@ namespace Barotrauma ); SettingCarouselElement prevDifficulty = difficultyOptions.FirstOrNull(element => element.Value == prevSettings.Difficulty) ?? difficultyOptions[1]; - SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions); + SettingValue difficultyInput = CreateSelectionCarousel(settingsList.Content, TextManager.Get("leveldifficulty"), TextManager.Get("leveldifficultyexplanation"), prevDifficulty, verticalSize, difficultyOptions, OnValuesChanged); SettingValue maxMissionCountInput = CreateGUINumberInputCarousel(settingsList.Content, TextManager.Get("maxmissioncount"), TextManager.Get("maxmissioncounttooltip"), prevSettings.MaxMissionCount, valueStep: 1, minValue: CampaignSettings.MinMissionCountLimit, maxValue: CampaignSettings.MaxMissionCountLimit, - verticalSize); + verticalSize, + OnValuesChanged); - presetDropdown.OnSelected = (selected, o) => + presetDropdown.OnSelected = (_, o) => { - if (o is CampaignSettings settings) - { - tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); - radiationEnabled.SetValue(settings.RadiationEnabled); - maxMissionCountInput.SetValue(settings.MaxMissionCount); - startingFundsInput.SetValue(settings.StartingBalanceAmount); - difficultyInput.SetValue(settings.Difficulty); - startingSetInput.SetValue(settings.StartItemSet); - return true; - } - return false; + if (o is not CampaignSettings settings) { return false; } + + loadingPreset = true; + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); + radiationEnabled.SetValue(settings.RadiationEnabled); + maxMissionCountInput.SetValue(settings.MaxMissionCount); + startingFundsInput.SetValue(settings.StartingBalanceAmount); + difficultyInput.SetValue(settings.Difficulty); + startingSetInput.SetValue(settings.StartItemSet); + loadingPreset = false; + return true; }; + void OnValuesChanged() + { + if (loadingPreset) { return; } + presetDropdown.Select(0); + } + return new CampaignSettingElements { + SelectedPreset = presetValue, TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, @@ -261,7 +283,7 @@ namespace Barotrauma }; // Create a number input with plus and minus buttons because for some reason the default GUINumberInput buttons don't work when in a GUIMessageBox - static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize) + static SettingValue CreateGUINumberInputCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, int defaultValue, int valueStep, int minValue, int maxValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -286,9 +308,11 @@ namespace Barotrauma minusButton.OnClicked = plusButton.OnClicked = ChangeValue; + numberInput.OnValueChanged += _ => onChanged(); + bool ChangeValue(GUIButton btn, object userData) { - if (!(userData is int change)) { return false; } + if (userData is not int change) { return false; } numberInput.IntValue += change; return true; @@ -298,7 +322,7 @@ namespace Barotrauma } static SettingValue CreateSelectionCarousel(GUIComponent parent, LocalizedString description, LocalizedString tooltip, SettingCarouselElement defaultValue, float verticalSize, - ImmutableArray> options) + ImmutableArray> options, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, horizontalSize: 0.55f, verticalSize: verticalSize); @@ -349,6 +373,8 @@ namespace Barotrauma return true; } + numberInput.OnValueChanged += _ => onChanged(); + void SetValue(int value) { numberInput.IntValue = value; @@ -358,7 +384,7 @@ namespace Barotrauma return new SettingValue(() => options[numberInput.IntValue].Value, t => SetValue(options.IndexOf(e => Equals(e.Value, t)))); } - static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize) + static SettingValue CreateTickbox(GUIComponent parent, LocalizedString description, LocalizedString tooltip, bool defaultValue, float verticalSize, Action onChanged) { GUILayoutGroup inputContainer = CreateSettingBase(parent, description, tooltip, 0.7f, verticalSize); GUILayoutGroup tickboxContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), inputContainer.RectTransform), childAnchor: Anchor.Center); @@ -370,6 +396,13 @@ namespace Barotrauma tickBox.Box.IgnoreLayoutGroups = true; tickBox.Box.RectTransform.SetPosition(Anchor.CenterRight); inputContainer.RectTransform.Parent.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + tickBox.OnSelected += _ => + { + onChanged(); + return true; + }; + return new SettingValue(() => tickBox.Selected, b => tickBox.Selected = b); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index a3ae05b14..c23b2b190 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -194,7 +194,7 @@ namespace Barotrauma { TextGetter = () => { - int initialMoney = CurrentSettings.InitialMoney; + int initialMoney = CampaignSettings.CurrentSettings.InitialMoney; if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; @@ -208,15 +208,15 @@ namespace Barotrauma { OnClicked = (tb, userdata) => { - CreateCustomizeWindow(CurrentSettings, settings => + CreateCustomizeWindow(CampaignSettings.CurrentSettings, settings => { - CampaignSettings prevSettings = CurrentSettings; - CurrentSettings = settings; + CampaignSettings prevSettings = CampaignSettings.CurrentSettings; + CampaignSettings.CurrentSettings = settings; if (prevSettings.InitialMoney != settings.InitialMoney) { object selectedData = subList.SelectedData; UpdateSubList(SubmarineInfo.SavedSubmarines); - if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CurrentSettings.InitialMoney) + if (selectedData is SubmarineInfo selectedSub && selectedSub.Price <= CampaignSettings.CurrentSettings.InitialMoney) { subList.Select(selectedData); } @@ -375,6 +375,7 @@ namespace Barotrauma { onClosed?.Invoke(elements.CreateSettings()); + GameSettings.SaveCurrentConfig(); return CampaignCustomizeSettings.Close(button, o); }; } @@ -399,7 +400,7 @@ namespace Barotrauma SubmarineInfo selectedSub = null; - if (!(subList.SelectedData is SubmarineInfo)) { return false; } + if (subList.SelectedData is not SubmarineInfo) { return false; } selectedSub = subList.SelectedData as SubmarineInfo; if (selectedSub.SubmarineClass == SubmarineClass.Undefined) @@ -419,7 +420,7 @@ namespace Barotrauma string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - CampaignSettings settings = CurrentSettings; + CampaignSettings settings = CampaignSettings.CurrentSettings; if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) { @@ -476,7 +477,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is SubmarineInfo sub)) { return; } + if (child.UserData is not SubmarineInfo sub) { return; } child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -487,9 +488,9 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - if (!(obj is SubmarineInfo sub)) { return true; } + if (obj is not SubmarineInfo sub) { return true; } #if !DEBUG - if (sub.Price > CurrentSettings.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney && !GameMain.DebugDraw) { SetPage(0); nextButton.Enabled = false; @@ -551,7 +552,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignSettings.CurrentSettings.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), @@ -563,7 +564,7 @@ namespace Barotrauma #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) + if (sub.Price > CampaignSettings.CurrentSettings.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -573,7 +574,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CurrentSettings.InitialMoney).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignSettings.CurrentSettings.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 00591d5b6..6cb2671ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -81,11 +81,21 @@ namespace Barotrauma tabs[(int)CampaignMode.InteractionType.Map] = CreateDefaultTabContainer(container, new Vector2(0.9f)); var mapFrame = new GUIFrame(new RectTransform(Vector2.One, GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); - new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var mapContainer = new GUICustomComponent(new RectTransform(Vector2.One, mapFrame.RectTransform), DrawMap, UpdateMap); + var notificationFrame = new GUIFrame(new RectTransform(new Point(mapContainer.Rect.Width, GUI.IntScale(40)), mapContainer.RectTransform, Anchor.BottomCenter), style: "ChatBox"); + new GUIFrame(new RectTransform(Vector2.One, mapFrame.RectTransform), style: "InnerGlow", color: Color.Black * 0.9f) { CanBeFocused = false + }; + + var notificationContainer = new GUICustomComponent(new RectTransform(new Vector2(0.98f, 1.0f), notificationFrame.RectTransform, Anchor.Center), DrawMapNotifications, null) + { + HideElementsOutsideFrame = true }; + var notificationHeader = new GUIImage(new RectTransform(new Vector2(0.1f, 1.0f), notificationFrame.RectTransform, Anchor.CenterLeft), style: "GUISlopedHeaderRight"); + var text = new GUITextBlock(new RectTransform(Vector2.One, notificationHeader.RectTransform, Anchor.Center), TextManager.Get("breakingnews"), font: GUIStyle.LargeFont); + notificationHeader.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.3f), 0); // crew tab ------------------------------------------------------------------------- @@ -152,18 +162,23 @@ namespace Barotrauma CreateUI(tabs[(int)CampaignMode.InteractionType.Map].Parent); } - GameMain.GameSession?.Map?.Draw(spriteBatch, mapContainer); + Campaign?.Map?.Draw(Campaign, spriteBatch, mapContainer); + } + + private void DrawMapNotifications(SpriteBatch spriteBatch, GUICustomComponent notificationContainer) + { + Campaign?.Map?.DrawNotifications(spriteBatch, notificationContainer); } private void UpdateMap(float deltaTime, GUICustomComponent mapContainer) { - var map = GameMain.GameSession?.Map; + var map = Campaign?.Map; if (map == null) { return; } - if (selectedLocation != null && selectedLocation == GameMain.GameSession.Campaign.GetCurrentDisplayLocation()) + if (selectedLocation != null && selectedLocation == Campaign.GetCurrentDisplayLocation()) { map.SelectLocation(-1); } - map.Update(deltaTime, mapContainer); + map.Update(Campaign, deltaTime, mapContainer); foreach (GUITickBox tickBox in missionTickBoxes) { bool disable = hasMaxMissions && !tickBox.Selected; @@ -260,14 +275,20 @@ namespace Barotrauma if (connection?.LevelData != null) { + if (location.Faction?.Prefab != null) + { + var factionLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), + TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight, textColor: location.Faction.Prefab.IconColor); + } var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)connection.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight); + if (connection.LevelData.HasBeaconStation) { var beaconStationContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -328,12 +349,31 @@ namespace Barotrauma if (connection != null && connection.Locations.Contains(currentDisplayLocation)) { List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); - if (!availableMissions.Contains(null)) { availableMissions.Insert(0, null); } + + if (!availableMissions.Any()) { availableMissions.Insert(0, null); } + + availableMissions.AddRange(location.AvailableMissions.Where(m => m.Locations[0] == m.Locations[1])); missionList.Content.ClearChildren(); + bool isPrevMissionInNextLocation = false; foreach (Mission mission in availableMissions) { + bool isMissionInNextLocation = mission != null && location.AvailableMissions.Contains(mission); + if (isMissionInNextLocation && !isPrevMissionInNextLocation) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), TextManager.Get("outpostmissions"), + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) + { + CanBeFocused = false + }; + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionList.Content.RectTransform), style: "HorizontalLine") + { + CanBeFocused = false + }; + } + isPrevMissionInNextLocation = isMissionInNextLocation; + var missionPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), missionList.Content.RectTransform), style: null) { UserData = mission @@ -347,45 +387,54 @@ namespace Barotrauma var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(15)); - if (mission != null) - { - var tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) + if (mission == null) + { + missionTextContent.RectTransform.MinSize = missionName.RectTransform.MinSize = new Point(0, GUI.IntScale(35)); + missionTextContent.ChildAnchor = Anchor.CenterLeft; + } + else + { + GUITickBox tickBox = null; + if (!isMissionInNextLocation) { - UserData = mission, - Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false - }; - tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); - tickBox.RectTransform.IsFixedSize = true; - tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); - tickBox.OnSelected += (GUITickBox tb) => - { - if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - - if (tb.Selected) + tickBox = new GUITickBox(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, label: string.Empty) { - Campaign.Map.CurrentLocation.SelectMission(mission); - } - else + UserData = mission, + Selected = Campaign.Map.CurrentLocation?.SelectedMissions.Contains(mission) ?? false + }; + tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); + tickBox.RectTransform.IsFixedSize = true; + tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap); + tickBox.OnSelected += (GUITickBox tb) => { - Campaign.Map.CurrentLocation.DeselectMission(mission); - } + if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } - foreach (GUITextBlock rewardText in missionRewardTexts) - { - Mission otherMission = rewardText.UserData as Mission; - rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); - } + if (tb.Selected) + { + Campaign.Map.CurrentLocation.SelectMission(mission); + } + else + { + Campaign.Map.CurrentLocation.DeselectMission(mission); + } - UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + foreach (GUITextBlock rewardText in missionRewardTexts) + { + Mission otherMission = rewardText.UserData as Mission; + rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); + } - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) - { - GameMain.Client?.SendCampaignState(); - } - return true; - }; - missionTickBoxes.Add(tickBox); + UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) + { + GameMain.Client?.SendCampaignState(); + } + return true; + }; + missionTickBoxes.Add(tickBox); + } GUILayoutGroup difficultyIndicatorGroup = null; if (mission.Difficulty.HasValue) @@ -410,7 +459,7 @@ namespace Barotrauma float extraPadding = 0;// 0.8f * tickBox.Rect.Width; float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; - missionName.Padding = new Vector4(missionName.Padding.X + tickBox.Rect.Width * 1.2f + extraPadding, + missionName.Padding = new Vector4(missionName.Padding.X + (tickBox?.Rect.Width ?? 0) * 1.2f + extraPadding, missionName.Padding.Y, missionName.Padding.Z + extraZPadding + extraPadding, missionName.Padding.W); @@ -425,9 +474,11 @@ namespace Barotrauma }; missionRewardTexts.Add(rewardText); - LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); - + LocalizedString reputationText = mission.GetReputationRewardText(); + if (!reputationText.IsNullOrEmpty()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.Description), wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); @@ -487,7 +538,7 @@ namespace Barotrauma OnClicked = (GUIButton btn, object obj) => { if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && - missionList.Content.Children.Any(c => c.UserData is Mission)) + missionList.Content.Children.Any(c => c.UserData is Mission mission && mission.Locations.Contains(Campaign?.Map?.CurrentLocation))) { var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => @@ -520,7 +571,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) + public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -541,7 +592,7 @@ namespace Barotrauma switch (selectedTab) { case CampaignMode.InteractionType.Store: - Store.SelectStore(storeIdentifier); + Store.SelectStore(npc); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 102992cae..b3e163a16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -87,26 +87,26 @@ namespace Barotrauma.CharacterEditor private float spriteSheetZoom = 1; private float spriteSheetMinZoom = 0.25f; private float spriteSheetMaxZoom = 1; - private int spriteSheetOffsetY = 20; - private int spriteSheetOffsetX = 30; + private const int spriteSheetOffsetY = 20; + private const int spriteSheetOffsetX = 30; private bool hideBodySheet; private Color backgroundColor = new Color(0.2f, 0.2f, 0.2f, 1.0f); private Vector2 cameraOffset; - private List selectedJoints = new List(); - private List selectedLimbs = new List(); - private HashSet editedCharacters = new HashSet(); + private readonly List selectedJoints = new List(); + private readonly List selectedLimbs = new List(); + private readonly HashSet editedCharacters = new HashSet(); private bool isEndlessRunner; private Rectangle spriteSheetRect; - private Rectangle CalculateSpritesheetRectangle() => + private Rectangle CalculateSpritesheetRectangle() => Textures == null || Textures.None() ? Rectangle.Empty : new Rectangle( - spriteSheetOffsetX, - spriteSheetOffsetY, - (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), + spriteSheetOffsetX, + spriteSheetOffsetY, + (int)(Textures.OrderByDescending(t => t.Width).First().Width * spriteSheetZoom), (int)(Textures.Sum(t => t.Height) * spriteSheetZoom)); private const string screenTextTag = "CharacterEditor."; @@ -143,7 +143,7 @@ namespace Barotrauma.CharacterEditor var humanSpeciesName = CharacterPrefab.HumanSpeciesName; if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllSpecies.First()); + SpawnCharacter(VisibleSpecies.First()); } else { @@ -192,7 +192,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allSpecies = null; + visibleSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -214,15 +214,12 @@ namespace Barotrauma.CharacterEditor private void Reset(IEnumerable characters = null) { - if (characters == null) - { - characters = editedCharacters; - } + characters ??= editedCharacters; characters.ForEach(c => ResetParams(c)); ResetVariables(); } - private void ResetParams(Character character) + private static void ResetParams(Character character) { character.Params.Reset(true); foreach (var animation in character.AnimController.AllAnimParams) @@ -719,7 +716,7 @@ namespace Barotrauma.CharacterEditor cameraOffset = Vector2.Clamp(cameraOffset, min, max); } Cam.Position = targetPos + cameraOffset; - MapEntity.mapEntityList.ForEach(e => e.IsHighlighted = false); + MapEntity.ClearHighlightedEntities(); // Update widgets jointSelectionWidgets.Values.ForEach(w => w.Update((float)deltaTime)); limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -994,7 +991,7 @@ namespace Barotrauma.CharacterEditor var collider = character.AnimController.Collider; var colliderDrawPos = SimToScreen(collider.SimPosition); Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); - var endPos = SimToScreen(collider.SimPosition + forward * collider.radius); + var endPos = SimToScreen(collider.SimPosition + forward * collider.Radius); GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green); GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue); Vector2 left = forward.Left(); @@ -1363,7 +1360,7 @@ namespace Barotrauma.CharacterEditor private class WallGroup { public readonly List walls; - + public WallGroup(List walls) { this.walls = walls; @@ -1374,7 +1371,7 @@ namespace Barotrauma.CharacterEditor var clones = new List(); walls.ForEachMod(w => clones.Add(w.Clone() as Structure)); return new WallGroup(clones); - } + } } private void CloneWalls() @@ -1391,7 +1388,7 @@ namespace Barotrauma.CharacterEditor else if (i == 2) { clones[i].walls[j].Move(new Vector2(-originalWall.walls[j].Rect.Width, 0)); - } + } } } } @@ -1404,8 +1401,8 @@ namespace Barotrauma.CharacterEditor private WallGroup SelectLastClone(bool right) { - var lastWall = right - ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() + var lastWall = right + ? clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Right).Last() : clones.SelectMany(c => c.walls).OrderBy(w => w.Rect.Left).First(); return clones.Where(c => c.walls.Contains(lastWall)).FirstOrDefault(); } @@ -1440,33 +1437,35 @@ namespace Barotrauma.CharacterEditor private Identifier currentCharacterIdentifier; private Identifier selectedJob = Identifier.Empty; - private List allSpecies; - private List AllSpecies + private List visibleSpecies; + private List VisibleSpecies { get { - if (allSpecies == null) - { -#if DEBUG - allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); -#else - allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); -#endif - allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); - } - return allSpecies; + visibleSpecies ??= CharacterPrefab.Prefabs.Where(ShowCreature).OrderBy(p => p.Identifier).Select(p => p.Identifier).ToList(); + return visibleSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private bool ShowCreature(CharacterPrefab prefab) + { + Identifier speciesName = prefab.Identifier; + if (speciesName == CharacterPrefab.HumanSpeciesName) { return true; } + if (!VanillaCharacters.Contains(prefab.ContentFile)) + { + // Always show all custom characters. + return true; + } + if (CreatureMetrics.UnlockAll) { return true; } + return CreatureMetrics.Unlocked.Contains(speciesName); + } + + private IEnumerable vanillaCharacters; + private IEnumerable VanillaCharacters { get { - if (vanillaCharacters == null) - { - vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); - } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); return vanillaCharacters; } } @@ -1475,7 +1474,7 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } @@ -1483,19 +1482,19 @@ namespace Barotrauma.CharacterEditor { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterIdentifier = AllSpecies[characterIndex]; + currentCharacterIdentifier = VisibleSpecies[characterIndex]; return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllSpecies.IndexOf(character.SpeciesName); + characterIndex = VisibleSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllSpecies.Count - 1) + if (characterIndex > VisibleSpecies.Count - 1) { characterIndex = 0; } @@ -1506,7 +1505,7 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllSpecies.Count - 1; + characterIndex = VisibleSpecies.Count - 1; } } @@ -1687,7 +1686,7 @@ namespace Barotrauma.CharacterEditor XElement overrideElement = null; if (duplicate != null) { - allSpecies = null; + visibleSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1823,9 +1822,9 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllSpecies.Contains(name)) + if (!VisibleSpecies.Contains(name)) { - AllSpecies.Add(name); + VisibleSpecies.Add(name); } SpawnCharacter(name, ragdollParams); limbPairEditing = false; @@ -2678,23 +2677,33 @@ namespace Barotrauma.CharacterEditor { Stretch = true }; - // Character selection var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); - var characterDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.2f), content.RectTransform) { RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllSpecies) + foreach (CharacterPrefab prefab in CharacterPrefab.Prefabs.OrderByDescending(p => p.Identifier)) { - characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); + Identifier speciesName = prefab.Identifier; + if (ShowCreature(prefab)) + { + characterDropDown.AddItem(speciesName.Value.CapitaliseFirstInvariant(), speciesName).SetAsFirstChild(); + } + else if (!CreatureMetrics.Encountered.Contains(speciesName)) + { + // Using a matching placeholder string here ("hidden"). + var element = characterDropDown.AddItem(TextManager.Get("hiddensubmarines"), Identifier.Empty, textColor: Color.Gray * 0.75f); + element.SetAsLastChild(); + element.Enabled = false; + } } characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { Identifier characterIdentifier = (Identifier)data; + if (characterIdentifier.IsEmpty) { return true; } try { SpawnCharacter(characterIdentifier); @@ -2795,7 +2804,7 @@ namespace Barotrauma.CharacterEditor saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; @@ -2835,7 +2844,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -2973,7 +2982,7 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) + if (VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); @@ -3212,7 +3221,7 @@ namespace Barotrauma.CharacterEditor Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); } - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -4235,7 +4244,7 @@ namespace Barotrauma.CharacterEditor int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; float GetWaveLength() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveLength) * Cam.Zoom / lengthMultiplier; - Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.radius) * 3 * Cam.Zoom; + Vector2 GetRefPoint() => SimToScreen(collider.SimPosition) - GetScreenSpaceForward() * ConvertUnits.ToDisplayUnits(collider.Radius) * 3 * Cam.Zoom; Vector2 GetDrawPos() => GetRefPoint() - GetScreenSpaceForward() * GetWaveLength(); Vector2 GetDir() => GetRefPoint() - GetDrawPos(); Vector2 GetStartPoint() => GetDrawPos() + GetDir() / 2; @@ -5008,9 +5017,9 @@ namespace Barotrauma.CharacterEditor // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. float multiplier = 0.9f; l.body.SetSize(new Vector2(size.X, size.Y) * l.Scale * RagdollParams.TextureScale * multiplier); - TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.Radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.Width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.Height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); } private void RecalculateOrigin(Limb l, Vector2? newOrigin = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index e191d3e54..43968f02e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; namespace Barotrauma { @@ -8,7 +6,7 @@ namespace Barotrauma { private GUIListBox listBox; - private ContentXElement configElement; + private readonly ContentXElement configElement; private float scrollSpeed; @@ -37,6 +35,8 @@ namespace Barotrauma set { listBox.BarScroll = value; } } + public readonly GUIButton CloseButton; + public CreditsPlayer(RectTransform rectT, string configFile) : base(null, rectT) { @@ -51,6 +51,10 @@ namespace Barotrauma configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); + + CloseButton = new GUIButton(new RectTransform(new Vector2(0.1f), RectTransform, Anchor.BottomRight, maxSize: new Point(GUI.IntScale(300), GUI.IntScale(50))) + { AbsoluteOffset = new Point(GUI.IntScale(20), GUI.IntScale(20) + (Rect.Bottom - GameMain.GraphicsHeight)) }, + TextManager.Get("close")); } private void Load() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 1edb85ae3..e99c35bce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Lights; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -14,9 +15,9 @@ namespace Barotrauma private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetFinal; - private Effect damageEffect; - private Texture2D damageStencil; - private Texture2D distortTexture; + private readonly Effect damageEffect; + private readonly Texture2D damageStencil; + private readonly Texture2D distortTexture; private float fadeToBlackState; @@ -106,13 +107,13 @@ namespace Barotrauma c.DoVisibilityCheck(cam); if (c.IsVisible != wasVisible) { - c.AnimController.Limbs.ForEach(l => + foreach (var limb in c.AnimController.Limbs) { - if (l.LightSource != null) + if (limb.LightSource is LightSource light) { - l.LightSource.Enabled = c.IsVisible; + light.Enabled = c.IsVisible; } - }); + } } } @@ -188,6 +189,10 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:LOS", sw.ElapsedTicks); sw.Restart(); + + static bool IsFromOutpostDrawnBehindSubs(Entity e) + => e.Submarine is { Info.OutpostGenerationParams.DrawBehindSubs: true }; + //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTarget); graphics.Clear(Color.Transparent); @@ -195,7 +200,7 @@ namespace Barotrauma //(= the background texture that's revealed when a wall is destroyed) into the background render target //These will be visible through the LOS effect. spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && !IsFromOutpostDrawnBehindSubs(e)); Submarine.DrawPaintedColors(spriteBatch, false); spriteBatch.End(); @@ -222,7 +227,11 @@ namespace Barotrauma Level.Loaded.DrawBack(graphics, spriteBatch, cam); } - //draw alpha blended particles that are in water and behind subs + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + Submarine.DrawBack(spriteBatch, false, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null) && IsFromOutpostDrawnBehindSubs(e)); + spriteBatch.End(); + + //draw alpha blended particles that are in water and behind subs #if LINUX || OSX spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); #else @@ -448,6 +457,11 @@ namespace Barotrauma Vector3 chromaticAberrationStrength = GameSettings.CurrentConfig.Graphics.ChromaticAberration ? new Vector3(-0.02f, -0.01f, 0.0f) : Vector3.Zero; + if (Level.Loaded?.Renderer != null) + { + chromaticAberrationStrength += new Vector3(-0.03f, -0.015f, 0.0f) * Level.Loaded.Renderer.ChromaticAberrationStrength; + } + if (Character.Controlled != null) { BlurStrength = Character.Controlled.BlurStrength * 0.005f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 4537be4ac..753b14915 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -219,7 +219,7 @@ namespace Barotrauma currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; - var dummyLocations = GameSession.CreateDummyLocations(seed: currentLevelData.Seed); + var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index ba1814a9b..30be6ec43 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,7 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private GUIDropDown serverExecutableDropdown; + private GUIDropDown languageDropdown, serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton; private readonly GUIFrame modsButtonContainer; @@ -484,6 +484,11 @@ namespace Barotrauma var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); + creditsPlayer.CloseButton.OnClicked = (btn, userdata) => + { + SelectTab(Tab.Empty); + return true; + }; } private void CreateTutorialTab() @@ -913,12 +918,14 @@ namespace Barotrauma #endif } - string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + - " -public " + isPublicBox.Selected.ToString() + - " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + - " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + - " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -maxplayers " + maxPlayersBox.Text; + string arguments = + "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + + " -public " + isPublicBox.Selected.ToString() + + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + + " -karmaenabled " + (!karmaBox.Selected).ToString() + + " -maxplayers " + maxPlayersBox.Text + + $" -language \"{(LanguageIdentifier)languageDropdown.SelectedData}\""; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) { @@ -1059,22 +1066,29 @@ namespace Barotrauma #if UNSTABLE backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); #endif - backgroundSprite ??= (LocationType.Prefabs.Where(l => l.UseInMainMenu).GetRandomUnsynced())?.GetPortrait(0); - } - - if (backgroundSprite != null) - { - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, - aberrationStrength: 0.0f); + if (GUIStyle.GetComponentStyle("MainMenuBackground") is { } mainMenuStyle && + mainMenuStyle.Sprites.TryGetValue(GUIComponent.ComponentState.None, out var sprites)) + { + backgroundSprite = sprites.GetRandomUnsynced()?.Sprite; + } + backgroundSprite ??= LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(0); } var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + float vignetteScale = Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y); + + Rectangle drawArea = new Rectangle( + (int)(vignette.size.X * vignetteScale / 2), 0, + (int)(GameMain.GraphicsWidth - vignette.size.X * vignetteScale / 2), GameMain.GraphicsHeight); + + if (backgroundSprite?.Texture != null) + { + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White, drawArea); + } + if (vignette != null) { - spriteBatch.Begin(blendState: BlendState.NonPremultiplied); - vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, - new Vector2(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y)); - spriteBatch.End(); + vignette.Draw(spriteBatch, Vector2.Zero, Color.White, Vector2.Zero, 0.0f, vignetteScale); } } @@ -1087,10 +1101,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + DrawBackground(graphics, spriteBatch); + GUI.Draw(Cam, spriteBatch); if (selectedTab != Tab.Credits) @@ -1120,7 +1134,7 @@ namespace Barotrauma if (i == 0) { GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); - if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) + if (mouseOn && PlayerInput.PrimaryMouseButtonClicked() && GUI.MouseOn == null) { GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } @@ -1229,45 +1243,28 @@ namespace Barotrauma { menuTabs[Tab.HostServer].ClearChildren(); - string name = ""; - string password = ""; - int maxPlayers = 8; - bool isPublic = true; - bool banAfterWrongPassword = false; - bool karmaEnabled = true; - string selectedKarmaPreset = ""; - PlayStyle selectedPlayStyle = PlayStyle.Casual; - if (File.Exists(ServerSettings.SettingsFile)) + var serverSettings = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile, out _)?.Root ?? new XElement("serversettings"); + + var name = serverSettings.GetAttributeString("name", ""); + var password = serverSettings.GetAttributeString("password", ""); + var isPublic = serverSettings.GetAttributeBool("IsPublic", true); + var banAfterWrongPassword = serverSettings.GetAttributeBool("banafterwrongpassword", false); + + int maxPlayersElement = serverSettings.GetAttributeInt("maxplayers", 8); + if (maxPlayersElement > NetConfig.MaxPlayers) { - XDocument settingsDoc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); - if (settingsDoc != null) - { - name = settingsDoc.Root.GetAttributeString("name", name); - password = settingsDoc.Root.GetAttributeString("password", password); - isPublic = settingsDoc.Root.GetAttributeBool("public", isPublic); - banAfterWrongPassword = settingsDoc.Root.GetAttributeBool("banafterwrongpassword", banAfterWrongPassword); - - int maxPlayersElement = settingsDoc.Root.GetAttributeInt("maxplayers", maxPlayers); - if (maxPlayersElement > NetConfig.MaxPlayers) - { - DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); - maxPlayersElement = NetConfig.MaxPlayers; - } - - maxPlayers = maxPlayersElement; - karmaEnabled = settingsDoc.Root.GetAttributeBool("karmaenabled", true); - selectedKarmaPreset = settingsDoc.Root.GetAttributeString("karmapreset", "default"); - string playStyleStr = settingsDoc.Root.GetAttributeString("playstyle", "Casual"); - Enum.TryParse(playStyleStr, out selectedPlayStyle); - } + DebugConsole.AddWarning($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead."); } + int maxPlayers = Math.Clamp(maxPlayersElement, min: 1, max: NetConfig.MaxPlayers); + + var karmaEnabled = serverSettings.GetAttributeBool("karmaenabled", true); + var selectedPlayStyle = serverSettings.GetAttributeEnum("playstyle", PlayStyle.Casual); Vector2 textLabelSize = new Vector2(1.0f, 0.05f); Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.04f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.95f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.01f, Stretch = true @@ -1345,7 +1342,7 @@ namespace Barotrauma //other settings ----------------------------------------------------- //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), text: name, textAlignment: textAlignment) @@ -1397,6 +1394,21 @@ namespace Barotrauma }; label.RectTransform.IsFixedSize = true; + var languageLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("Language"), textAlignment: textAlignment); + languageDropdown = new GUIDropDown(new RectTransform(textFieldSize, languageLabel.RectTransform, Anchor.CenterRight)); + foreach (var language in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(language.Label, language.Identifier); + } + var defaultLanguage = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + var settingsLanguage = serverSettings.GetAttributeIdentifier("language", defaultLanguage.Value).ToLanguageIdentifier(); + if (!ServerLanguageOptions.Options.Any(o => o.Identifier == settingsLanguage)) + { + settingsLanguage = defaultLanguage; + } + languageDropdown.Select(ServerLanguageOptions.Options.FindIndex(o => o.Identifier == settingsLanguage)); + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerExecutable"), textAlignment: textAlignment); const string vanillaServerOption = "Vanilla"; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 81c40bd0d..95b889acf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -400,12 +400,9 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); //wtf - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 59514c304..244167045 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -301,7 +301,7 @@ namespace Barotrauma levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); - backgroundSprite = LocationType.Random(new MTRandom(intSeed))?.GetPortrait(intSeed); + backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); SeedBox.Text = levelSeed; } } @@ -1934,7 +1934,7 @@ namespace Barotrauma var selectedSub = component.UserData as SubmarineInfo; if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { - if (selectedSub.Price > CampaignSetupUI.CurrentSettings.InitialMoney) + if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); } @@ -2758,12 +2758,10 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { + if (backgroundSprite?.Texture == null) { return; } graphics.Clear(Color.Black); - - GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); - spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } @@ -3315,7 +3313,7 @@ namespace Barotrauma foreach (var subElement in SubList.Content.Children) { var sub = subElement.UserData as SubmarineInfo; - bool tooExpensive = sub.Price > CampaignSetupUI.CurrentSettings.InitialMoney; + bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney; if (tooExpensive || !sub.IsCampaignCompatible) { foreach (var textBlock in subElement.GetAllChildren()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index c48aa0ac5..56c74dcc3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -166,7 +166,7 @@ namespace Barotrauma { prefabList.ClearChildren(); - var particlePrefabs = GameMain.ParticleManager.GetPrefabList(); + var particlePrefabs = ParticleManager.GetPrefabList(); foreach (ParticlePrefab particlePrefab in particlePrefabs) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), prefabList.Content.RectTransform) { MinSize = new Point(0, 20) }, @@ -204,7 +204,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab prefab in prefabList) { foreach (XElement element in doc.Root.Elements()) @@ -273,7 +273,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var prefabList = GameMain.ParticleManager.GetPrefabList(); + var prefabList = ParticleManager.GetPrefabList(); foreach (ParticlePrefab otherPrefab in prefabList) { foreach (var subElement in doc.Root.Elements()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index f86553cb7..5d1831408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Xml.Linq; namespace Barotrauma @@ -241,6 +239,7 @@ namespace Barotrauma private GUITickBox filterPassword; private GUITickBox filterFull; private GUITickBox filterEmpty; + private GUIDropDown languageDropdown; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; @@ -255,6 +254,7 @@ namespace Barotrauma private TernaryOption filterModdedValue = TernaryOption.Any; private ColumnLabel sortedBy; + private bool sortedAscending = true; private const float sidebarWidth = 0.2f; public ServerListScreen() @@ -425,10 +425,13 @@ namespace Barotrauma ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); + RectTransform createFilterRectT() + => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform); + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { text ??= TextManager.Get(key); - var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + var tickBox = new GUITickBox(createFilterRectT(), text) { UserData = text, Selected = defaultState, @@ -450,6 +453,109 @@ namespace Barotrauma filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); + // Language filter + if (ServerLanguageOptions.Options.Any()) + { + var languageKey = "Language".ToIdentifier(); + var allLanguagesKey = "AllLanguages".ToIdentifier(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + + languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true); + + languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey); + var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild(); + + // Spacer between "All" and the individual languages + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform) + { + MinSize = new Point(0, GUI.IntScaleCeiling(2)) + }, style: null) + { + Color = Color.DarkGray, + CanBeFocused = false + }; + + var selectedLanguages + = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray( + languageKey, + Array.Empty()); + foreach (var (label, identifier, _) in ServerLanguageOptions.Options) + { + languageDropdown.AddItem(label, identifier); + } + + if (!selectedLanguages.Any()) + { + selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray(); + } + + foreach (var lang in selectedLanguages) + { + languageDropdown.SelectItem(lang); + } + + if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l))) + { + languageDropdown.SelectItem(allLanguagesKey); + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + + var langTickboxes = languageDropdown.ListBox.Content.Children + .Where(c => c.UserData is LanguageIdentifier) + .Select(c => c.GetChild()) + .ToArray(); + + bool inSelectedCall = false; + languageDropdown.OnSelected = (_, userData) => + { + if (inSelectedCall) { return true; } + try + { + inSelectedCall = true; + + if (Equals(allLanguagesKey, userData)) + { + foreach (var tb in langTickboxes) + { + tb.Selected = allTickbox.Selected; + } + } + + bool noneSelected = langTickboxes.All(tb => !tb.Selected); + bool allSelected = langTickboxes.All(tb => tb.Selected); + + if (allSelected != allTickbox.Selected) + { + allTickbox.Selected = allSelected; + } + + if (allSelected) + { + languageDropdown.Text = TextManager.Get(allLanguagesKey); + } + else if (noneSelected) + { + languageDropdown.Text = TextManager.Get("None"); + } + + var languages = languageDropdown.SelectedDataMultiple.OfType(); + + ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages)); + GameSettings.SaveCurrentConfig(); + return true; + } + finally + { + inSelectedCall = false; + FilterServers(); + } + }; + } + // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { @@ -713,7 +819,7 @@ namespace Barotrauma private void SortList(ColumnLabel sortBy, bool toggle) { - if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { return; } + if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; } sortedBy = sortBy; @@ -730,51 +836,74 @@ namespace Barotrauma } } - bool ascending = arrowUp.Visible; + sortedAscending = arrowUp.Visible; if (toggle) { - ascending = !ascending; + sortedAscending = !sortedAscending; } - arrowUp.Visible = ascending; - arrowDown.Visible = !ascending; + arrowUp.Visible = sortedAscending; + arrowDown.Visible = !sortedAscending; serverList.Content.RectTransform.SortChildren((c1, c2) => { - if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } - if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } - - switch (sortBy) - { - case ColumnLabel.ServerListCompatible: - bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); - bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); - - if (s1Compatible == s2Compatible) { return 0; } - return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListHasPassword: - if (s1.HasPassword == s2.HasPassword) { return 0; } - return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListName: - // I think we actually want culture-specific sorting here? - return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); - case ColumnLabel.ServerListRoundStarted: - if (s1.GameStarted == s2.GameStarted) { return 0; } - return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPlayers: - return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); - case ColumnLabel.ServerListPing: - return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch - { - (false, false) => 0, - (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), - (false, true) => 1, - (true, false) => -1 - }; - default: - return 0; - } + if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } + if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } + int comparison = sortedAscending ? 1 : -1; + return CompareServer(sortBy, s1, s2) * comparison; }); } + + private void InsertServer(ServerInfo serverInfo, GUIComponent component) + { + var children = serverList.Content.RectTransform.Children.Reverse().ToList(); + + int comparison = sortedAscending ? 1 : -1; + foreach (var child in children) + { + if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } + if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0) + { + var index = serverList.Content.RectTransform.GetChildIndex(child); + component.RectTransform.RepositionChildInHierarchy(index + 1); + return; + } + } + component.RectTransform.SetAsFirstChild(); + } + + private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2) + { + switch (sortBy) + { + case ColumnLabel.ServerListCompatible: + bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); + bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); + + if (s1Compatible == s2Compatible) { return 0; } + return s1Compatible ? -1 : 1; + case ColumnLabel.ServerListHasPassword: + if (s1.HasPassword == s2.HasPassword) { return 0; } + return s1.HasPassword ? 1 : -1; + case ColumnLabel.ServerListName: + // I think we actually want culture-specific sorting here? + return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture); + case ColumnLabel.ServerListRoundStarted: + if (s1.GameStarted == s2.GameStarted) { return 0; } + return s1.GameStarted ? 1 : -1; + case ColumnLabel.ServerListPlayers: + return s2.PlayerCount.CompareTo(s1.PlayerCount); + case ColumnLabel.ServerListPing: + return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch + { + (false, false) => 0, + (true, true) => s2Ping.CompareTo(s1Ping), + (false, true) => 1, + (true, false) => -1 + }; + default: + return 0; + } + } public override void Select() { @@ -821,6 +950,7 @@ namespace Barotrauma UpdateFriendsList(); panelAnimator?.Update(); + scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -840,7 +970,7 @@ namespace Barotrauma RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) { - if (!(child.UserData is ServerInfo serverInfo)) { continue; } + if (child.UserData is not ServerInfo serverInfo) { continue; } child.Visible = ShouldShowServer(serverInfo); } @@ -851,6 +981,20 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } + private bool AllLanguagesVisible + { + get + { + if (languageDropdown is null) { return true; } + + // CountChildren-1 because there's a separator element in there that can't be selected + int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1; + int selectedCount = languageDropdown.SelectedIndexMultiple.Count(); + + return selectedCount >= tickBoxCount; + } + } + private bool ShouldShowServer(ServerInfo serverInfo) { #if !DEBUG @@ -918,6 +1062,14 @@ namespace Barotrauma } } + if (!AllLanguagesVisible) + { + if (!languageDropdown.SelectedDataMultiple.OfType().Contains(serverInfo.Language)) + { + return false; + } + } + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (Identifier)tickBox.UserData; @@ -1031,8 +1183,8 @@ namespace Barotrauma if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } if (info.IsInServer - && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } - && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) + && info.ConnectCommand.TryUnwrap(out var command) + && command.EndpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { const int framePadding = 5; @@ -1270,7 +1422,7 @@ namespace Barotrauma serverPreview.Content.ClearChildren(); panelAnimator.RightEnabled = false; joinButton.Enabled = false; - selectedServer = null; + selectedServer = Option.None; if (selectedTab == TabEnum.All) { @@ -1370,8 +1522,7 @@ namespace Barotrauma UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } - SortList(sortedBy, toggle: false); - FilterServers(); + InsertServer(serverInfo, serverFrame); } private void UpdateServerInfoUI(ServerInfo serverInfo) @@ -1629,7 +1780,7 @@ namespace Barotrauma #endif } - private Color GetPingTextColor(int ping) + private static Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); @@ -1640,12 +1791,10 @@ namespace Barotrauma graphics.Clear(Color.CornflowerBlue); GameMain.TitleScreen.DrawLoadingText = false; - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); } @@ -1666,6 +1815,7 @@ namespace Barotrauma { ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } + GameSettings.SaveCurrentConfig(); } public void LoadServerFilters() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs new file mode 100644 index 000000000..458614977 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -0,0 +1,173 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; + +namespace Barotrauma +{ + class SlideshowPlayer : GUIComponent + { + private readonly SlideshowPrefab slideshowPrefab; + private readonly LocalizedString pressAnyKeyText; + + private int state; + + private Color overlayColor, textColor; + + private float timer; + + private LocalizedString currentText; + + public bool LastTextShown => state >= slideshowPrefab.Slides.Length; + public bool Finished => state > slideshowPrefab.Slides.Length; + + public SlideshowPlayer(RectTransform rectT, SlideshowPrefab prefab) : base(null, rectT) + { + slideshowPrefab = prefab; + overlayColor = Color.Black; + textColor = Color.Transparent; + pressAnyKeyText = TextManager.Get("pressanykey"); + RefreshText(); + } + + public void Restart() + { + state = 0; + } + + public void Finish() + { + state = slideshowPrefab.Slides.Length + 1; + } + + protected override void Update(float deltaTime) + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if (!Visible || (Finished && timer > slide.FadeOutDuration)) { return; } + + timer += deltaTime; + + if (state == 0) + { + overlayColor = Color.Lerp(Color.Black, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + else + { + overlayColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.FadeInDelay) / slide.FadeInDuration, 1.0f)); + } + + if (timer > slide.TextFadeInDelay) + { + textColor = Color.Lerp(Color.Transparent, Color.White, Math.Min((timer - slide.TextFadeInDelay) / slide.TextFadeInDuration, 1.0f)); + if (AnyKeyHit()) + { + if (timer > slide.TextFadeInDelay + slide.FadeInDuration) + { + overlayColor = textColor = Color.Transparent; + timer = 0.0f; + state++; + RefreshText(); + } + else + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + } + else + { + textColor = Color.Transparent; + if (AnyKeyHit()) + { + timer = slide.TextFadeInDelay + slide.TextFadeInDuration; + } + } + + if (state >= slideshowPrefab.Slides.Length) + { + overlayColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + textColor = Color.Lerp(Color.White, Color.Transparent, Math.Min(timer / slide.FadeOutDuration, 1.0f)); + if (timer >= slide.FadeOutDuration) + { + state++; + RefreshText(); + } + } + + static bool AnyKeyHit() + { + return + PlayerInput.GetKeyboardState.GetPressedKeys().Any(k => PlayerInput.KeyHit(k)) || + PlayerInput.PrimaryMouseButtonClicked(); + } + } + + private void RefreshText() + { + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + currentText = slide.Text + .Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown") + .Replace("[location]", Level.Loaded?.StartOutpost?.Info.Name ?? "Unknown"); + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (slideshowPrefab.Slides.IsEmpty) { return; } + + var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)]; + if ((Finished && timer > slide.FadeOutDuration)) { return; } + + var overlaySprite = slide.Portrait; + + if (overlaySprite != null) + { + Sprite prevPortrait = null; + if (state > 0 && state < slideshowPrefab.Slides.Length) + { + prevPortrait = slideshowPrefab.Slides[state - 1].Portrait; + DrawOverlay(prevPortrait, Color.White); + } + if (prevPortrait?.Texture != overlaySprite.Texture) + { + DrawOverlay(overlaySprite, overlayColor); + } + } + else + { + GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); + } + + if (!currentText.IsNullOrEmpty() && textColor.A > 0) + { + var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); + Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; + LocalizedString wrappedText = ToolBox.WrapText(currentText, GameMain.GraphicsWidth / 3, GUIStyle.Font); + Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); + Vector2 textPos = centerPos - textSize / 2; + backgroundSprite.Draw(spriteBatch, + centerPos, + Color.White * (textColor.A / 255.0f), + origin: backgroundSprite.size / 2, + rotate: 0.0f, + scale: new Vector2(GameMain.GraphicsWidth / 2 / backgroundSprite.size.X, textSize.Y / backgroundSprite.size.Y * 2.0f)); + + GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (textColor.A / 255.0f)); + GUI.DrawString(spriteBatch, textPos, wrappedText, textColor); + + if (timer > slide.TextFadeInDelay * 2) + { + float alpha = Math.Min(timer - slide.TextFadeInDelay * 2, 1.0f); + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(pressAnyKeyText) / 2; + GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, pressAnyKeyText, Color.Black * (textColor.A / 255.0f) * alpha); + GUI.DrawString(spriteBatch, bottomTextPos, pressAnyKeyText, textColor * alpha); + } + } + + void DrawOverlay(Sprite sprite, Color color) + { + if (sprite.Texture == null) { return; } + GUI.DrawBackgroundSprite(spriteBatch, sprite, color); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 9546ea05f..e4d07a87f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -1159,7 +1159,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in entityLists[categoryKey]) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } @@ -1178,7 +1178,7 @@ namespace Barotrauma foreach (MapEntityPrefab ep in MapEntityPrefab.List) { #if !DEBUG - if (ep.HideInMenus) { continue; } + if (ep.HideInMenus && !GameMain.DebugDraw) { continue; } #endif CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); } @@ -1307,7 +1307,6 @@ namespace Barotrauma try { assemblyPrefab.Delete(); - UpdateEntityList(); OpenEntityMenu(MapEntityCategory.ItemAssembly); } catch (Exception e) @@ -2419,6 +2418,17 @@ namespace Barotrauma return true; } }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("beaconstationplacement")) + { + Selected = MainSub.Info.BeaconStationInfo is { Placement: Level.PlacementType.Top }, + OnSelected = (tb) => + { + MainSub.Info.BeaconStationInfo.Placement = tb.Selected ? + Level.PlacementType.Top : + Level.PlacementType.Bottom; + return true; + } + }; beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); //------------------------------------------------------------------ @@ -3082,9 +3092,17 @@ namespace Barotrauma string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } - + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); - doc.SaveSafe(filePath); + try + { + doc.SaveSafe(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to save the item assembly to \"{filePath}\".", e); + return; + } var result = ContentPackageManager.ReloadContentPackage(existingContentPackage); if (!result.TryUnwrapSuccess(out var resultPackage)) @@ -3638,6 +3656,8 @@ namespace Barotrauma private void OpenEntityMenu(MapEntityCategory? entityCategory) { + UpdateEntityList(); + foreach (GUIButton categoryButton in entityCategoryButtons) { categoryButton.Selected = entityCategory.HasValue ? @@ -3765,14 +3785,14 @@ namespace Barotrauma { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? - MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : + List targets = MapEntity.HighlightedEntities.Any(me => !MapEntity.SelectedList.Contains(me)) ? + MapEntity.HighlightedEntities.ToList() : new List(MapEntity.SelectedList); Item target = null; var single = targets.Count == 1 ? targets.Single() : null; - if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && ic is not Repairable && ic.GuiFrame != null)) { // Do not offer the ability to open the inventory if the inventory should never be drawn var container = item.GetComponent(); @@ -4017,7 +4037,7 @@ namespace Barotrauma pickerMutex = new object(), hexMutex = new object(); - Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); + Vector2 relativeSize = new Vector2(0.4f * GUI.AspectRatioAdjustment, 0.3f); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { @@ -4056,24 +4076,31 @@ namespace Barotrauma GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight); float currentHue = colorPicker.SelectedHue / 360f; - GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; - GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; - GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; - GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) }, + inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; - GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; + GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.1f + }; - new GUICustomComponent(new RectTransform(new Vector2(0.4f, 0.8f), colorInfoLayout.RectTransform), (batch, component) => + new GUICustomComponent(new RectTransform(Vector2.One, colorInfoLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), (batch, component) => { Rectangle rect = component.Rect; Point areaSize = new Point(rect.Width, rect.Height / 2); @@ -5245,9 +5272,9 @@ namespace Barotrauma { if (dummyCharacter.SelectedItem == null) { - foreach (var entity in MapEntity.mapEntityList) + foreach (var entity in MapEntity.HighlightedEntities) { - if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null)) { var container = item.GetComponents().ToList(); if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index a5e37dcdb..7cf2b716b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -49,7 +49,7 @@ namespace Barotrauma } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "assistant")); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain")); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); dummyCharacter.Info.GiveExperience(999999); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs index 48bde97dc..d96f57b72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -1,20 +1,18 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { - #warning TODO: implement properly public class ServerListFilters { private readonly Dictionary attributes = new Dictionary(); - private ServerListFilters() { } - - private ServerListFilters(XElement elem) + private ServerListFilters(XElement? elem) { - if (elem == null) { return; } + if (elem is null) { return; } foreach (var attr in elem.Attributes()) { attributes.Add(attr.NameAsIdentifier(), attr.Value); @@ -23,8 +21,6 @@ namespace Barotrauma public static void Init(XElement? elem) { - if (elem is null) { return; } - Instance = new ServerListFilters(elem); } @@ -50,17 +46,27 @@ namespace Barotrauma { if (attributes.TryGetValue(key, out string? val)) { - if (Enum.TryParse(val, out T result)) { return result; } + if (Enum.TryParse(val, ignoreCase: true, out T result)) { return result; } } return def; } + public LanguageIdentifier[] GetAttributeLanguageIdentifierArray(Identifier key, LanguageIdentifier[] def) + { + return attributes.TryGetValue(key, out string? val) + ? val.Split(",") + .Select(static s => s.Trim()) + .Where(static s => !s.IsNullOrWhiteSpace()) + .Select(static s => s.ToLanguageIdentifier()).ToArray() + : def; + } + public void SetAttribute(Identifier key, string val) { attributes[key] = val; } - public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(null); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 64ba580ef..f53f49be0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -176,6 +176,10 @@ namespace Barotrauma void updateWaterAmbience(Sound sound, float volume) { SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); + if (Level.Loaded != null) + { + volume *= Level.Loaded.GenerationParams.WaterAmbienceVolume; + } if (chn is null || !chn.IsPlaying) { if (volume < 0.01f) { return; } @@ -513,6 +517,11 @@ namespace Barotrauma if (musicDisposed) { Thread.Sleep(60); } } + + public static void ForceMusicUpdate() + { + updateMusicTimer = 0.0f; + } private static void UpdateMusic(float deltaTime) { @@ -540,7 +549,7 @@ namespace Barotrauma IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); int mainTrackIndex = 0; - if (suitableMusic.Count() == 0) + if (suitableMusic.None()) { targetMusic[mainTrackIndex] = null; } @@ -564,11 +573,21 @@ namespace Barotrauma } } - if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) + if (Level.Loaded != null && (Level.Loaded.Type == LevelData.LevelType.LocationConnection || Level.Loaded.GenerationParams.PlayNoiseLoopInOutpostLevel)) { + Identifier biome = Level.Loaded.LevelData.Biome.Identifier; + if (Level.Loaded.IsEndBiome && GameMain.GameSession?.Campaign is CampaignMode campaign) + { + //don't play end biome music in the path leading up to the end level(s) + if (!campaign.Map.EndLocations.Contains(Level.Loaded.StartLocation)) + { + biome = Level.Loaded.StartLocation.Biome.Identifier; + } + } + // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips(Level.Loaded.LevelData.Biome.Identifier, currentIntensity) : + GetSuitableMusicClips(biome, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { @@ -597,10 +616,17 @@ namespace Barotrauma targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } + IEnumerable suitableIntensityMusic = Enumerable.Empty(); + if (targetMusic[mainTrackIndex] is { MuteIntensityTracks: false } mainTrack && Screen.Selected == GameMain.GameScreen) + { + float intensity = currentIntensity; + if (mainTrack?.ForceIntensityTrack != null) + { + intensity = mainTrack.ForceIntensityTrack.Value; + } + suitableIntensityMusic = GetSuitableMusicClips("intensity".ToIdentifier(), intensity); + } //get the appropriate intensity layers for current situation - IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : - Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -729,6 +755,14 @@ namespace Barotrauma firstTimeInMainMenu = false; + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + var missionMusic = mission.GetOverrideMusicType(); + if (!missionMusic.IsEmpty) { return missionMusic; } + } + } if (Character.Controlled != null) { @@ -754,6 +788,11 @@ namespace Barotrauma } } + if (Level.Loaded is { IsEndBiome: true }) + { + return "endlevel".ToIdentifier(); + } + Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { @@ -765,8 +804,8 @@ namespace Barotrauma return "deep".ToIdentifier(); } - if (targetSubmarine != null) - { + if (targetSubmarine != null) + { float floodedArea = 0.0f; float totalArea = 0.0f; foreach (Hull hull in Hull.HullList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 14b0b68e2..bb3af5eee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -238,17 +238,24 @@ namespace Barotrauma public readonly float Volume; public readonly Vector2 IntensityRange; + public readonly bool MuteIntensityTracks; + public readonly float? ForceIntensityTrack; public readonly bool ContinueFromPreviousTime; public int PreviousTime; public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) { - Type = element.GetAttributeIdentifier("type", ""); - IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + Type = element.GetAttributeIdentifier(nameof(Type), ""); + IntensityRange = element.GetAttributeVector2(nameof(IntensityRange), new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool(nameof(DuckVolume), false); + MuteIntensityTracks = element.GetAttributeBool(nameof(MuteIntensityTracks), false); + if (element.GetAttribute(nameof(ForceIntensityTrack)) != null) + { + ForceIntensityTrack = element.GetAttributeFloat(nameof(ForceIntensityTrack), 0.0f); + } + Volume = element.GetAttributeFloat(nameof(Volume), 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool(nameof(ContinueFromPreviousTime), false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 765fa93ca..39407e976 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -188,7 +188,6 @@ namespace Barotrauma public float GetRotation(ref float rotationState, float randomRotationFactor) { - RotationSpeed = -Math.Abs(RotationSpeed); switch (RotationAnim) { case AnimationType.Sine: diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 9927c01b8..41be59e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -72,8 +72,9 @@ namespace Barotrauma float angle = 0.0f; float particleRotation = 0.0f; bool mirrorAngle = false; - if (emitter.Prefab.Properties.CopyEntityAngle) + if (emitter.Prefab.Properties.CopyEntityAngle || emitter.Prefab.Properties.CopyTargetAngle) { + bool entityAngleAssigned = false; Limb targetLimb = null; if (entity is Item item && item.body != null) { @@ -84,19 +85,23 @@ namespace Barotrauma particleRotation += MathHelper.Pi; mirrorAngle = true; } + entityAngleAssigned = true; } - else if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + if (emitter.Prefab.Properties.CopyTargetAngle || !entityAngleAssigned) { - targetLimb = c.AnimController.GetLimb(l); - } - else - { - for (int i = 0; i < targets.Count; i++) + if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { - if (targets[i] is Limb limb) + targetLimb = c.AnimController.GetLimb(l); + } + else + { + for (int i = 0; i < targets.Count; i++) { - targetLimb = limb; - break; + if (targets[i] is Limb limb) + { + targetLimb = limb; + break; + } } } } @@ -108,14 +113,19 @@ namespace Barotrauma particleRotation += offset; if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f) { - particleRotation += MathHelper.Pi; - mirrorAngle = true; + angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + particleRotation = -targetLimb.body.Rotation; + if (targetLimb.body.Dir < 0.0f) + { + particleRotation += MathHelper.Pi; + mirrorAngle = true; + } } } } emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation, mirrorAngle: mirrorAngle); - } + } } private bool ignoreMuffling; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 258594f3d..1c1cda216 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -117,6 +117,7 @@ namespace Barotrauma.Steam currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + currentLobby?.SetData("language", serverSettings.Language.ToString()); DebugConsole.Log("Lobby updated!"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs index 7a3a9b85d..12b4b2876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ModListPreset.cs @@ -113,7 +113,7 @@ namespace Barotrauma { case ModType.Workshop: pkgElem.SetAttributeValue("name", pkg.Name); - if (pkg.UgcId.TryUnwrap(out ContentPackageId ugcId)) + if (pkg.UgcId.TryUnwrap(out var ugcId)) { pkgElem.SetAttributeValue("id", ugcId.ToString()); } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index ab98cb73f..a87e71868 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index bfc6b83de..fe0503287 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 085183249..62d47a79d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 6bfd80a2b..ef131028c 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 5af3d13ab..f1f8fdac2 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a8b32422a..0f0014017 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -71,6 +71,11 @@ namespace Barotrauma msg.WriteString(ragdollFileName); msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier); + msg.WriteIdentifier(MinReputationToHire.factionId); + if (MinReputationToHire.factionId != default) + { + msg.WriteSingle(MinReputationToHire.reputation); + } if (Job != null) { msg.WriteUInt32(Job.Prefab.UintIdentifier); @@ -86,7 +91,7 @@ namespace Barotrauma msg.WriteByte((byte)0); } - msg.WriteUInt16((ushort)ExperiencePoints); + msg.WriteInt32(ExperiencePoints); msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 9958908e4..8feb516d2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -696,6 +696,7 @@ namespace Barotrauma { msg.WriteIdentifier(MerchantIdentifier); } + msg.WriteIdentifier(Faction); int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index ca7d95a63..eb5b340da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1428,6 +1428,44 @@ namespace Barotrauma })); + commands.Add(new Command("forcelocationtypechange", "", (string[] args) => + { + if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; } + + if (args.Length < 2) + { + ThrowError("Invalid parameters. The command should be formatted as \"forcelocationtypechange [locationname] [locationtype]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (location == null) + { + ThrowError($"Could not find a location with the name {args[0]}."); + return; + } + + var locationType = LocationType.Prefabs.FirstOrDefault(lt => + lt.Name.Equals(args[1], StringComparison.OrdinalIgnoreCase) || lt.Identifier == args[1]); + if (location == null) + { + ThrowError($"Could not find the location type {args[1]}."); + return; + } + + location.ChangeType(GameMain.GameSession.Campaign, locationType); + }, + () => + { + if (GameMain.GameSession?.Campaign == null) { return null; } + + return new string[][] + { + GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(), + LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray() + }; + })); + AssignOnExecute("resetcharacternetstate", (string[] args) => { if (GameMain.Server == null) { return; } @@ -1699,13 +1737,7 @@ namespace Barotrauma "teleportsub", (Client client, Vector2 cursorWorldPos, string[] args) => { - if (Submarine.MainSub == null || Level.Loaded == null) return; - if (Level.Loaded.Type == LevelData.LevelType.Outpost) - { - GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red); - return; - } - + if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(cursorWorldPos); @@ -1961,7 +1993,7 @@ namespace Barotrauma { GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red); } - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); } ); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index c4a043bbd..353ef08d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -41,7 +41,7 @@ namespace Barotrauma clientsToRemove.Add(k); } } - if (!(clientsToRemove is null)) + if (clientsToRemove is not null) { foreach (var k in clientsToRemove) { @@ -62,7 +62,7 @@ namespace Barotrauma { foreach (Entity e in targets) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -85,7 +85,7 @@ namespace Barotrauma IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) { - if (!(e is Character character) || !character.IsRemotePlayer) { continue; } + if (e is not Character character || !character.IsRemotePlayer) { continue; } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character); if (targetClient != null) { @@ -149,5 +149,15 @@ namespace Barotrauma } GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } + + public void ServerWriteSelectedOption(Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION); + outmsg.WriteUInt16(Identifier); + outmsg.WriteByte((byte)(selectedOption + 1)); + GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs new file mode 100644 index 000000000..5369ec7e1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/MissionAction.cs @@ -0,0 +1,43 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma +{ + partial class MissionAction : EventAction + { + private static readonly HashSet missionsUnlockedThisRound = new HashSet(); + + public static void ResetMissionsUnlockedThisRound() + { + missionsUnlockedThisRound.Clear(); + } + + public static void NotifyMissionsUnlockedThisRound(Client client) + { + foreach (Mission mission in missionsUnlockedThisRound) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission) + { + foreach (Client client in GameMain.Server.ConnectedClients) + { + NotifyMissionUnlock(mission, client); + } + } + + private static void NotifyMissionUnlock(Mission mission, Client client) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); + outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); + outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[0]) ?? -1); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[1]) ?? -1); + outmsg.WriteString(mission.Name.Value); + GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 5d9dde87c..62b7f9882 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -14,12 +14,12 @@ namespace Barotrauma foreach (Event ev in activeEvents) { - if (!(ev is ScriptedEvent scriptedEvent)) { continue; } + if (ev is not ScriptedEvent scriptedEvent) { continue; } var actions = FindActions(scriptedEvent); foreach (EventAction action in actions.Select(a => a.Item2)) { - if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; } + if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; } if (!convAction.TargetClients.Contains(sender)) { #if DEBUG || UNSTABLE @@ -42,6 +42,14 @@ namespace Barotrauma else { convAction.SelectedOption = selectedOption; + if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption)) + { + foreach (Client c in convAction.TargetClients) + { + if (c == sender) { continue; } + convAction.ServerWriteSelectedOption(c); + } + } } } return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..6284dd232 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EndMission.cs @@ -0,0 +1,19 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + base.ServerWriteInitial(msg, c); + + boss.WriteSpawnData(msg, boss.ID, restrictMessageSize: false); + msg.WriteByte((byte)minions.Length); + foreach (Character minion in minions) + { + minion.WriteSpawnData(msg, minion.ID, restrictMessageSize: false); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 52a944a4e..9653e372e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -6,33 +6,66 @@ namespace Barotrauma { partial class SalvageMission : Mission { - private bool usedExistingItem; + struct SpawnInfo + { + public readonly bool UsedExistingItem; + public readonly UInt16 OriginalInventoryID; + public readonly byte OriginalItemContainerIndex; + public readonly int OriginalSlotIndex; + public readonly List<(int listIndex, int effectIndex)> ExecutedEffectIndices; - private UInt16 originalInventoryID; - private byte originalItemContainerIndex; - private int originalSlotIndex; + public SpawnInfo(bool usedExistingItem, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex, List<(int listIndex, int effectIndex)> executedEffectIndices) + { + UsedExistingItem = usedExistingItem; + OriginalInventoryID = originalInventoryID; + OriginalItemContainerIndex = originalItemContainerIndex; + OriginalSlotIndex = originalSlotIndex; + ExecutedEffectIndices = executedEffectIndices; + } + } - private readonly List> executedEffectIndices = new List>(); + private readonly Dictionary spawnInfo = new Dictionary(); public override void ServerWriteInitial(IWriteMessage msg, Client c) { base.ServerWriteInitial(msg, c); - msg.WriteBoolean(usedExistingItem); - if (usedExistingItem) + foreach (var target in targets) { - msg.WriteUInt16(item.ID); - } - else - { - item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); - } + bool targetFound = spawnInfo.ContainsKey(target) && target.Item != null; + msg.WriteBoolean(targetFound); + if (!targetFound) { continue; } - msg.WriteByte((byte)executedEffectIndices.Count); - foreach (Pair effectIndex in executedEffectIndices) + msg.WriteBoolean(spawnInfo[target].UsedExistingItem); + if (spawnInfo[target].UsedExistingItem) + { + msg.WriteUInt16(target.Item.ID); + } + else + { + target.Item.WriteSpawnData(msg, + target.Item.ID, + spawnInfo[target].OriginalInventoryID, + spawnInfo[target].OriginalItemContainerIndex, + spawnInfo[target].OriginalSlotIndex); + } + + msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count); + foreach ((int listIndex, int effectIndex) in spawnInfo[target].ExecutedEffectIndices) + { + msg.WriteByte((byte)listIndex); + msg.WriteByte((byte)effectIndex); + } + } + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + msg.WriteByte((byte)targets.Count); + for (int i = 0; i < targets.Count; i++) { - msg.WriteByte((byte)effectIndex.First); - msg.WriteByte((byte)effectIndex.Second); + msg.WriteByte((byte)targets[i].State); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 89ff137db..618e48512 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -88,9 +88,6 @@ namespace Barotrauma Console.WriteLine("Loading game settings"); GameSettings.Init(); - Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.Cache.Load(); - Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); @@ -189,7 +186,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-name": name = CommandLineArgs[i + 1]; @@ -262,7 +259,7 @@ namespace Barotrauma for (int i = 0; i < CommandLineArgs.Length; i++) { - switch (CommandLineArgs[i].Trim()) + switch (CommandLineArgs[i].Trim().ToLowerInvariant()) { case "-playstyle": Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle); @@ -284,6 +281,14 @@ namespace Barotrauma Server.ServerSettings.KarmaPreset = karmaPresetName; i++; break; + case "-language": + LanguageIdentifier language = CommandLineArgs[i + 1].ToLanguageIdentifier(); + if (ServerLanguageOptions.Options.Any(o => o.Identifier == language)) + { + Server.ServerSettings.Language = language; + } + i++; + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 82810135f..b037e9888 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -85,6 +85,7 @@ namespace Barotrauma { if (purchasedHullRepairs == value) { return; } purchasedHullRepairs = value; + PurchasedHullRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -95,6 +96,7 @@ namespace Barotrauma { if (purchasedLostShuttles == value) { return; } purchasedLostShuttles = value; + PurchasedLostShuttlesInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -105,6 +107,7 @@ namespace Barotrauma { if (purchasedItemRepairs == value) { return; } purchasedItemRepairs = value; + PurchasedItemRepairsInLatestSave |= value; IncrementLastUpdateIdForFlag(NetFlags.Misc); } } @@ -337,11 +340,12 @@ namespace Barotrauma IsFirstRound = true; break; case TransitionType.ProgressToNextEmptyLocation: + Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; } - Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration); + Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) @@ -391,17 +395,14 @@ namespace Barotrauma NextLevel = newLevel; MirrorLevel = mirror; - //give clients time to play the end cinematic before starting the next round - if (transitionType == TransitionType.End) - { - yield return new WaitForSeconds(EndCinematicDuration); - } - else - { - yield return new WaitForSeconds(EndTransitionDuration * 0.5f); - } - GameMain.Server.TryStartGame(); + yield return new WaitForSeconds(EndTransitionDuration * 0.5f); + + //don't start the next round automatically if we just finished the campaign + if (transitionType != TransitionType.End) + { + GameMain.Server.TryStartGame(); + } yield return CoroutineStatus.Success; } @@ -424,7 +425,7 @@ namespace Barotrauma } public bool CanPurchaseSub(SubmarineInfo info, Client client) - => CanAfford(info.Price, client) && GetCampaignSubs().Contains(info); + => CanAfford(info.GetPrice(), client) && GetCampaignSubs().Contains(info); private readonly List discardedCharacters = new List(); public void DiscardClientCharacterData(Client client) @@ -493,7 +494,8 @@ namespace Barotrauma if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); - if (transitionType == TransitionType.End) + if (transitionType == TransitionType.End || + (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation)) { LoadNewLevel(); } @@ -509,6 +511,14 @@ namespace Barotrauma } } } + else if (Level.Loaded.IsEndBiome) + { + var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); + if (transitionType == TransitionType.ProgressToNextLocation) + { + LoadNewLevel(); + } + } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { KeepCharactersCloseToOutpost(deltaTime); @@ -704,10 +714,6 @@ namespace Barotrauma if (requiredFlags.HasFlag(NetFlags.Reputation)) { msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation)); - Reputation reputation = Map?.CurrentLocation?.Reputation; - msg.WriteBoolean(reputation != null); - if (reputation != null) { msg.WriteSingle(reputation.Value); } - // hopefully we'll never have more than 128 factions msg.WriteByte((byte)Factions.Count); foreach (Faction faction in Factions) @@ -823,54 +829,37 @@ namespace Barotrauma Bank.ForceUpdate(); } - if (purchasedHullRepairs != PurchasedHullRepairs) + if (purchasedHullRepairs && !PurchasedHullRepairs) { - switch (purchasedHullRepairs) + if (GetBalance(sender) >= hullRepairCost) { - case true when GetBalance(sender) >= hullRepairCost: - TryPurchase(sender, hullRepairCost); - PurchasedHullRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - break; - case false: - PurchasedHullRepairs = false; - personalWallet.Refund(hullRepairCost); - break; + TryPurchase(sender, hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); } } - if (purchasedItemRepairs != PurchasedItemRepairs) + if (purchasedItemRepairs && !PurchasedItemRepairs) { - switch (purchasedItemRepairs) + if (GetBalance(sender) >= itemRepairCost) { - case true when GetBalance(sender) >= itemRepairCost: - TryPurchase(sender, itemRepairCost); - PurchasedItemRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - break; - case false: - PurchasedItemRepairs = false; - personalWallet.Refund(itemRepairCost); - break; + TryPurchase(sender, itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); } } - if (purchasedLostShuttles != PurchasedLostShuttles) + if (purchasedLostShuttles && !PurchasedLostShuttles) { if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost)) + else if (TryPurchase(sender, shuttleRetrieveCost)) { PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } - else if (!purchasedItemRepairs) - { - PurchasedLostShuttles = false; - personalWallet.Refund(shuttleRetrieveCost); - } } if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) @@ -1021,12 +1010,13 @@ namespace Barotrauma bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } + var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); foreach (var (prefab, category, _) in purchasedUpgrades) { UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging - int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList); int level = UpgradeManager.GetUpgradeLevel(prefab, category); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); } @@ -1057,49 +1047,47 @@ namespace Barotrauma if (GameMain.Server is null) { return; } - switch (transfer.Sender) + if (transfer.Sender.TryUnwrap(out var id)) { - case Some { Value: var id }: - if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } + if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; } - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - TransferMoney(wallet); - break; - case None _: - if (!AllowedToManageWallets(sender)) + TransferMoney(wallet); + } + else + { + if (!AllowedToManageWallets(sender)) + { + if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID) { - if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) - { - if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } - GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); - GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); - } - return; + if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; } + GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money); } + return; + } - TransferMoney(Bank); - break; + TransferMoney(Bank); } void TransferMoney(Wallet from) { if (!from.TryDeduct(transfer.Amount)) { return; } - switch (transfer.Receiver) + if (transfer.Receiver.TryUnwrap(out var id)) { - case Some { Value: var id }: - Wallet wallet = GetWalletByID(id); - if (wallet is InvalidWallet) { return; } + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } - wallet.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; - case None _: - Bank.Give(transfer.Amount); - GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); - break; + wallet.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); + } + else + { + Bank.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e46134ca6..d6695caf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -24,7 +24,11 @@ namespace Barotrauma public DateTimeOffset Expiry; } - private readonly Dictionary rateLimits = new Dictionary(); + private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); + + private readonly List afflictionSubscribers = new(); + + private readonly Dictionary rateLimits = new(); public void ServerRead(IReadMessage inc, Client sender) { @@ -35,6 +39,9 @@ namespace Barotrauma case NetworkHeader.ADD_EVERYTHING_TO_PENDING: ProcessAddEverything(sender); break; + case NetworkHeader.UNSUBSCRIBE_ME: + RemoveClientSubscription(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -72,6 +79,17 @@ namespace Barotrauma ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } + private void RemoveClientSubscription(Client client) + { + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + } + } + } + private void ProcessNewRemoval(IReadMessage inc, Client client) { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } @@ -129,6 +147,14 @@ namespace Barotrauma Afflictions = pendingAfflictions }; + if (foundInfo is not null) + { + RemoveClientSubscription(client); + + // the client subscribes to the afflictions of the crew member for the next minute + afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1))); + } + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index de8517914..188721aa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -8,10 +8,12 @@ namespace Barotrauma.Items.Components private readonly struct EventData : IEventData { public readonly bool Launch; + public readonly byte SpreadCounter; - public EventData(bool launch) + public EventData(bool launch, byte spreadCounter = 0) { Launch = launch; + SpreadCounter = spreadCounter; } } @@ -32,6 +34,7 @@ namespace Barotrauma.Items.Components msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); + msg.WriteByte(eventData.SpreadCounter); } bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 8beb42942..fd03a0bfd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components { string newOutputValue = msg.ReadString(); - if (item.CanClientAccess(c)) + if (item.CanClientAccess(c) && !Readonly) { if (newOutputValue.Length > MaxMessageLength) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 3508a03ae..40c1ee0a2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -253,6 +253,7 @@ namespace Barotrauma msg.WriteBoolean(hasIdCard); if (hasIdCard) { + msg.WriteInt32(idCardComponent.SubmarineSpecificID); msg.WriteString(idCardComponent.OwnerName); msg.WriteString(idCardComponent.OwnerTags); msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index c2a1f8cc6..8742d17eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -140,8 +140,7 @@ namespace Barotrauma.Networking return Option.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime)); } - bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement) - .OfType>().Select(o => o.Value)); + bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement).NotNone()); } private void RemoveExpired() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 37ef60130..06928528a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1129,6 +1129,7 @@ namespace Barotrauma.Networking { //check if midround syncing is needed due to missed unique events if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); } + MissionAction.NotifyMissionsUnlockedThisRound(c); c.InGame = true; } } @@ -2388,10 +2389,7 @@ namespace Barotrauma.Networking List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); - if (Level.Loaded?.StartOutpost != null && - Level.Loaded.Type == LevelData.LevelType.Outpost && - (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && - Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && @@ -2466,7 +2464,6 @@ namespace Barotrauma.Networking spawnedCharacter.Info.InventoryData = new XElement("inventory"); spawnedCharacter.Info.StartItemsGiven = true; spawnedCharacter.SaveInventory(); - // talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway spawnedCharacter.LoadTalents(); } } @@ -3023,7 +3020,7 @@ namespace Barotrauma.Networking client.WaitForNextRoundRespawn = null; client.InGame = false; - if (client.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (client.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client)); if (previousPlayer == null) @@ -3366,12 +3363,13 @@ namespace Barotrauma.Networking if (checkActiveVote && Voting.ActiveVote != null) { +#warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?) var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame); - if (inGameClients.Count() == 1) + if (inGameClients.Count() == 1 && inGameClients.First() == Voting.ActiveVote.VoteStarter) { Voting.ActiveVote.Finish(Voting, passed: true); } - else + else if (inGameClients.Any()) { var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter); int yes = eligibleClients.Count(c => c.GetVote(Voting.ActiveVote.VoteType) == 2); @@ -3441,12 +3439,11 @@ namespace Barotrauma.Networking public void SwitchSubmarine() { - if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; } + if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; } SubmarineInfo targetSubmarine = subVote.Sub; VoteType voteType = Voting.ActiveVote.VoteType; Client starter = Voting.ActiveVote.VoteStarter; - int deliveryFee = 0; switch (voteType) { @@ -3456,7 +3453,6 @@ namespace Barotrauma.Networking GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: - deliveryFee = subVote.DeliveryFee; break; default: return; @@ -3464,7 +3460,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); } Voting.StopSubmarineVote(true); @@ -4030,8 +4026,7 @@ namespace Barotrauma.Networking } public void Quit() - { - + { if (started) { started = false; @@ -4043,7 +4038,7 @@ namespace Barotrauma.Networking ServerSettings.SaveSettings(); - ModSender.Dispose(); + ModSender?.Dispose(); if (ServerSettings.SaveServerLogs) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index e732ce117..737e9555d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -158,7 +158,7 @@ namespace Barotrauma else if (client.Karma < 40.0f) herpesStrength = 30.0f; - var existingAffliction = client.Character.CharacterHealth.GetAffliction("spaceherpes"); + var existingAffliction = client.Character.CharacterHealth.GetAffliction(AfflictionPrefab.SpaceHerpesType); if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f); } } @@ -358,8 +358,8 @@ namespace Barotrauma } } - bool targetIsHusk = target.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; - bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction("huskinfection")?.State == AfflictionHusk.InfectionState.Active; + bool targetIsHusk = target.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; + bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active; //huskified characters count as enemies to healthy characters and vice versa if (targetIsHusk != attackerIsHusk) { isEnemy = true; } @@ -614,7 +614,7 @@ namespace Barotrauma if (amount < 0.0f) { - float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); var clientMemory = GetClientMemory(client); clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 160eaf8a3..f2b222205 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -308,7 +308,7 @@ namespace Barotrauma.Networking { if (netServer == null) { return; } - PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId); + PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId); DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}"); if (pendingClient is null) @@ -316,7 +316,7 @@ namespace Barotrauma.Networking if (status == Steamworks.AuthResponse.OK) { return; } if (connectedClients.Find(c - => c.AccountInfo.AccountId is Some { Value: SteamId id } && id.Value == steamId) + => c.AccountInfo.AccountId.TryUnwrap(out var id) && id.Value == steamId) is LidgrenConnection connection) { Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status)); @@ -390,7 +390,7 @@ namespace Barotrauma.Networking lidgrenConn.Status = NetworkConnectionStatus.Disconnected; connectedClients.Remove(lidgrenConn); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket); - if (conn.AccountInfo.AccountId is Some { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); } + if (conn.AccountInfo.AccountId.TryUnwrap(out var steamId)) { SteamManager.StopAuthSession(steamId); } } lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 64e864b2a..a5fb3b598 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking protected List connectedClients = null!; protected List pendingClients = null!; protected ServerSettings serverSettings = null!; - protected Option ownerKey = null!; + protected Option ownerKey = Option.None; protected NetworkConnection? OwnerConnection; protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep) @@ -295,7 +295,7 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); - if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some { Value: SteamId steamId }) + if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap(out var steamId)) { Steam.SteamManager.StopAuthSession(steamId); pendingClient.Connection.SetAccountInfo(AccountInfo.None); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 52674581c..6bbbd8f58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -239,7 +239,10 @@ namespace Barotrauma.Networking foreach (Door door in shuttleDoors) { - if (door.IsOpen) door.TrySetState(false, false, true); + if (door.IsOpen) + { + door.TrySetState(open: false, isNetworkMessage: false, sendNetworkMessage: true); + } } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index d4777ea28..9bb8e46d7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); - + partial void InitProjSpecific() { LoadSettings(); @@ -176,7 +176,11 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log( + NetworkMember.ClientLogName(c) + + $" changed {netProperties[key].Name}" + + $" to {netProperties[key].Value}", + ServerLog.MessageType.ServerMessage); } propertiesChanged = true; } @@ -330,6 +334,10 @@ namespace Barotrauma.Networking { LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } + if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", ""))) + { + Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); + } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 3eaee94aa..787c786d6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -28,13 +28,11 @@ namespace Barotrauma public SubmarineInfo Sub; public bool TransferItems; - public int DeliveryFee; - public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType) + public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType) { Sub = subInfo; TransferItems = transferItems; - DeliveryFee = deliveryFee; VoteType = voteType; State = VoteState.Started; VoteStarter = starter; @@ -81,10 +79,10 @@ namespace Barotrauma if (passed) { Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet; - if (fromWallet.TryDeduct(TransferAmount)) + if (fromWallet != null && fromWallet.TryDeduct(TransferAmount)) { Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet; - toWallet.Give(TransferAmount); + toWallet?.Give(TransferAmount); } } else @@ -109,7 +107,6 @@ namespace Barotrauma sender, subInfo, transferItems, - voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); @@ -206,12 +203,16 @@ namespace Barotrauma // Do not take unanswered into account for total int yes = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 2); int no = eligibleClients.Count(c => c.GetVote(ActiveVote.VoteType) == 1); - int total = Math.Max(yes + no, 1); - - bool passed = - yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || - inGameClients.Count() == 1; + int total = yes + no; + bool passed = false; + //total can be zero if the client who initiated the vote has left + if (total > 0) + { + passed = + yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio || + inGameClients.Count() == 1; + } ActiveVote.Finish(this, passed); } } @@ -436,7 +437,6 @@ namespace Barotrauma var subVote = ActiveVote as SubmarineVote; msg.WriteString(subVote.Sub.Name); msg.WriteBoolean(subVote.TransferItems); - msg.WriteInt16((short)subVote.DeliveryFee); break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 002481c61..c9c5e57e0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -209,20 +209,24 @@ namespace Barotrauma public void RandomizeSettings() { - if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8); + if (GameMain.Server.ServerSettings.RandomizeSeed) { LevelSeed = ToolBox.RandomSeed(8); } - if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + //don't touch any of these settings if a campaign is running! + if (GameMain.GameSession?.Campaign == null) { - var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); - SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; - } - if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) - { - var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); - SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; - } + if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) + { + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList(); + SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; + } + if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) + { + var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign); + SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier; + } - GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + GameMain.Server.ServerSettings.SelectNonHiddenSubmarine(); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index d51559575..18d874395 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -72,6 +72,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); + Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString()); Steamworks.SteamServer.DedicatedServer = true; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index c3809dfea..fedfa09c4 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -11,7 +11,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.21.6.0 + 1.0.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/languageoptions.xml b/Barotrauma/BarotraumaShared/Data/languageoptions.xml new file mode 100644 index 000000000..f1fdbe04b --- /dev/null +++ b/Barotrauma/BarotraumaShared/Data/languageoptions.xml @@ -0,0 +1,22 @@ + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index f0eae6be8..131be5ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -168,6 +168,7 @@ namespace Barotrauma public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; public bool IsSteeringThroughGap { get; protected set; } + public bool IsTryingToSteerThroughGap { get; protected set; } public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { @@ -444,7 +445,7 @@ namespace Barotrauma if (EscapeTarget != null) { var door = EscapeTarget.ConnectedDoor; - bool isClosedDoor = door != null && !door.IsOpen; + bool isClosedDoor = door != null && door.IsClosed; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; float sqrDist = diff.LengthSquared(); bool isClose = sqrDist < MathUtils.Pow2(100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index bbe7863ec..156545868 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -245,11 +245,6 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } - if (Character.Params.Group == "human") - { - // Pet - Character.TeamID = CharacterTeamType.FriendlyNPC; - } var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement; targetMemories = new Dictionary(); steeringManager = outsideSteering; @@ -309,17 +304,20 @@ namespace Barotrauma break; } } - + //pets are friendly! + if (PetBehavior != null || Character.Group == "human") + { + Character.TeamID = CharacterTeamType.FriendlyNPC; + } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; - requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); myBodies.Add(Character.AnimController.Collider.FarseerBody); + CreatureMetrics.UnlockInEditor(Character.SpeciesName); } private CharacterParams.AIParams _aiParams; @@ -452,6 +450,7 @@ namespace Barotrauma base.Update(deltaTime); UpdateTriggers(deltaTime); Character.ClearInputs(); + IsTryingToSteerThroughGap = false; Reverse = false; bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); @@ -558,8 +557,9 @@ namespace Barotrauma } } - if (AIParams.CanOpenDoors) + if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors) { + // Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk). bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) @@ -584,6 +584,7 @@ namespace Barotrauma } else { + // Normally the monsters only use pathing inside submarines, not outside. if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) @@ -848,7 +849,7 @@ namespace Barotrauma IsSteeringThroughGap = false; if (SwarmBehavior != null) { - SwarmBehavior.IsActive = State == AIState.Idle && Character.CurrentHull == null; + SwarmBehavior.IsActive = SwarmBehavior.ForceActive || State == AIState.Idle && Character.CurrentHull == null; SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } @@ -876,7 +877,7 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { - if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f) { // Steer straight up if very deep SteeringManager.SteeringManual(deltaTime, Vector2.UnitY); @@ -1144,7 +1145,6 @@ namespace Barotrauma return; } } - attackLimbSelectionTimer -= deltaTime; if (AttackLimb == null || attackLimbSelectionTimer <= 0) { @@ -1154,7 +1154,8 @@ namespace Barotrauma AttackLimb = GetAttackLimb(attackWorldPos); } } - + Character targetCharacter = SelectedAiTarget.Entity as Character; + IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; bool canAttack = true; bool pursue = false; if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) @@ -1379,7 +1380,6 @@ namespace Barotrauma float distance = 0; Limb attackTargetLimb = null; - Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { if (!Character.AnimController.SimplePhysicsEnabled) @@ -1400,29 +1400,29 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + Vector2 toTargetOffset = toTarget; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) { if (wallTarget.Structure.Submarine != null) { Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } else if (targetCharacter != null) { Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); - toTarget += margin; + toTargetOffset += margin; } else if (SelectedAiTarget.Entity is MapEntity e) { if (e.Submarine != null) { Vector2 margin = CalculateMargin(e.Submarine.Velocity); - toTarget += margin; + toTargetOffset += margin; } } @@ -1430,7 +1430,7 @@ namespace Barotrauma { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } + if (diff <= 0 || toTargetOffset.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } float distanceOffset = diff * AttackLimb.attack.Duration; @@ -1439,7 +1439,7 @@ namespace Barotrauma } // Check that we can reach the target - distance = toTarget.Length(); + distance = toTargetOffset.Length(); canAttack = distance < AttackLimb.attack.Range; if (canAttack) { @@ -1523,20 +1523,18 @@ namespace Barotrauma } } Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; + bool updateSteering = true; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso); } - if (steeringLimb == null) { State = AIState.Idle; return; } - var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackLimb != null && AttackLimb.attack.Retreat) { UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1603,7 +1601,7 @@ namespace Barotrauma } } } - else + else if (!IsTryingToSteerThroughGap) { if (AttackLimb.attack.Ranged) { @@ -1624,6 +1622,10 @@ namespace Barotrauma SteeringManager.Reset(); } } + else + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + } } else { @@ -1662,40 +1664,60 @@ namespace Barotrauma if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } if (selectedTargetingParams == null) { break; } var targetSub = SelectedAiTarget.Entity?.Submarine; - if (targetSub == null) { break; } - float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; - float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + ISpatialEntity spatialTarget = targetSub ?? SelectedAiTarget.Entity; + float targetSize = 0; + if (!selectedTargetingParams.IgnoreTargetSize) + { + targetSize = + targetSub != null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 : + targetCharacter != null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100; + } + float sqrDistToTarget = Vector2.DistanceSquared(WorldPosition, spatialTarget.WorldPosition); + bool isProgressive = AIParams.MaxAggression - AIParams.StartAggression > 0; switch (CirclePhase) { case CirclePhase.Start: - currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, ClampIntensity(aggressionIntensity)); inverseDir = false; circleDir = GetDirFromHeadingInRadius(); circleRotation = 0; strikeTimer = 0; blockCheckTimer = 0; breakCircling = false; - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset; // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. - circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); - canAttack = false; + float ClampIntensity(float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f), AIParams.StartAggression, AIParams.MaxAggression); + if (isProgressive) + { + float intensity = ClampIntensity(currentAttackIntensity); + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity); + circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity)); + } + else + { + circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed; + circleFallbackDistance = maxFallBackDistance; + circleOffset = Rand.Vector(maxRandomOffset); + } + circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor); aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); - if (targetSub.Borders.Width < 1000) + DisableAttacksIfLimbNotRanged(); + if (targetSub != null && targetSub.Borders.Width < 1000 && AttackLimb?.attack is { Ranged: false }) { breakCircling = true; CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance)) { CirclePhase = CirclePhase.CloseIn; } - else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.FallBack; } @@ -1705,52 +1727,76 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + Vector2 targetVelocity = GetTargetVelocity(); + float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f: + targetSize + selectedTargetingParams.CircleStartDistance / 2; + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetVelocity)) { strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } - else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) + else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.Advance; } - canAttack = false; + DisableAttacksIfLimbNotRanged(); break; case CirclePhase.FallBack: + updateSteering = false; bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); - if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) + if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Advance; break; } - return; + DisableAttacksIfLimbNotRanged(); + break; case CirclePhase.Advance: - Vector2 subSpeed = targetSub.Velocity; - float requiredDistMultiplier = 1; - // If the target sub is moving fast, just steer towards the target until close enough to strike - if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + Vector2 targetVel = GetTargetVelocity(); + // If the target is moving fast, just steer towards the target + if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed())) { CirclePhase = CirclePhase.CloseIn; } + else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f)) + { + if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100) + { + circleRotationSpeed *= 1 + deltaTime; + } + else + { + CirclePhase = CirclePhase.CloseIn; + } + } else { - circleRotation += deltaTime * circleRotationSpeed * circleDir; - if (circleRotation < -360) + float rotationStep = circleRotationSpeed * deltaTime * circleDir; + if (isProgressive) { - circleRotation += 360; + circleRotation += rotationStep; } - else if (circleRotation > 360) + else { - circleRotation -= 360; + circleRotation = rotationStep; } Vector2 targetPos = attackSimPos + circleOffset; - if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + float targetDist = targetSize; + if (targetDist <= 0) + { + targetDist = circleFallbackDistance; + } + if (targetSub != null && AttackLimb?.attack is { Ranged: true }) + { + targetDist += circleFallbackDistance / 2; + } + if (Vector2.DistanceSquared(SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist)) { // Too close to the target point // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (canAttack && AttackLimb?.attack is { Ranged: false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; strikeTimer = AttackLimb.attack.CoolDown; @@ -1762,7 +1808,6 @@ namespace Barotrauma break; } steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); if (IsBlocked(deltaTime, steerPos)) { if (!inverseDir) @@ -1774,7 +1819,7 @@ namespace Barotrauma else if (circleRotationSpeed < 1) { // Then try increasing the rotation speed to change the movement curve - circleRotationSpeed *= 1.1f; + circleRotationSpeed *= 1 + deltaTime; } else if (circleOffset.LengthSquared() > 0.1f) { @@ -1784,16 +1829,24 @@ namespace Barotrauma else { // If we still fail, just steer towards the target - breakCircling = true; + breakCircling = AttackLimb?.attack is { Ranged: false }; + if (!breakCircling) + { + CirclePhase = CirclePhase.FallBack; + } } } } - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb?.attack is { Ranged: false }) { - strikeTimer = AttackLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; + canAttack = false; + float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel); + if (distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + { + strikeTimer = AttackLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } } - canAttack = false; break; case CirclePhase.Strike: strikeTimer -= deltaTime; @@ -1815,18 +1868,19 @@ namespace Barotrauma return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; } - float GetStrikeDistanceMultiplier(Vector2 subSpeed) + float GetStrikeDistanceMultiplier(Vector2 targetVelocity) { + if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) { return 0; } float requiredDistMultiplier = 2; - bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; if (isHeading) { requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; - float subSpeedHorizontal = Math.Abs(subSpeed.X); - if (subSpeedHorizontal > 1) + float targetVelocityHorizontal = Math.Abs(targetVelocity.X); + if (targetVelocityHorizontal > 1) { // Reduce the required distance if the target is moving. - requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1)); if (requiredDistMultiplier < 2) { requiredDistMultiplier = 2; @@ -1843,19 +1897,59 @@ namespace Barotrauma return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; } - float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + Vector2 GetTargetVelocity() + { + if (targetSub != null) + { + return targetSub.Velocity; + } + else if (targetCharacter != null) + { + return targetCharacter.AnimController.Collider.LinearVelocity; + } + return Vector2.Zero; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f)); } } - if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + if (updateSteering) { - bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; - bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); - if (fallBack) + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) { - Reverse = true; - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f; + bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); + if (fallBack) + { + Reverse = true; + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + else if (advance) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else + { + if (Character.CurrentHull == null && !canAttack) + { + SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } + else + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } + } } - else if (advance) + else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1866,41 +1960,18 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else + if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || + distance == 0 || + distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2) || + AttackLimb != null && AttackLimb.attack.Ranged)) { - if (Character.CurrentHull == null && !canAttack) - { - SteeringManager.SteeringWander(avoidWanderingOutsideLevel: true); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); - } - else - { - SteeringManager.Reset(); - FaceTarget(SelectedAiTarget.Entity); - } + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } } - else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) - { - if (pathSteering != null) - { - pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); - } - else - { - SteeringManager.SteeringSeek(steerPos, 10); - } - } - - 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); - } } } Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; - IDamageable damageTarget = targetEntity as IDamageable; - if (AttackLimb?.attack is Attack { Ranged: true} attack) + if (AttackLimb?.attack is Attack { Ranged: true } attack) { AimRangedAttack(attack, targetEntity); } @@ -1915,12 +1986,21 @@ namespace Barotrauma { AttackLimb.attack.ResetAttackTimer(); } + + void DisableAttacksIfLimbNotRanged() + { + if (AttackLimb?.attack is { Ranged: false }) + { + canAttack = false; + } + } } public void AimRangedAttack(Attack attack, Entity targetEntity) { if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; } Character.SetInput(InputType.Aim, false, true); + if (attack.AimRotationTorque <= 0) { return; } Limb limb = GetLimbToRotate(attack); if (limb != null) { @@ -2003,9 +2083,18 @@ namespace Barotrauma float prio = 1 + limb.attack.Priority; if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); - // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. - // We also need a max value that is more than the actual range. - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + float distanceFactor = 1; + if (limb.attack.Ranged) + { + float min = 100; + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist)); + } + else + { + // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. + // We also need a max value that is more than the actual range. + distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); + } return prio * distanceFactor; } } @@ -2164,7 +2253,9 @@ namespace Barotrauma { if (SelectedAiTarget?.Entity == null) { return false; } if (AttackLimb?.attack == null) { return false; } - if (damageTarget == null) { return false; } + ISpatialEntity spatialTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as ISpatialEntity; + if (spatialTarget == null) { return false; } + ActiveAttack = AttackLimb.attack; if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -2176,13 +2267,14 @@ namespace Barotrauma return true; } } + if (damageTarget == null) { return false; } ActiveAttack = AttackLimb.attack; if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) { Limb referenceLimb = GetLimbToRotate(ActiveAttack); if (referenceLimb != null) { - Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition; + Vector2 toTarget = spatialTarget.WorldPosition - referenceLimb.WorldPosition; float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir); float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); @@ -2200,16 +2292,20 @@ namespace Barotrauma { if (item.RequireAimToUse) { - if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + if (!Aim(deltaTime, spatialTarget, item)) { // Valid target, but can't shoot -> return true so that it will not be ignored. return true; } } - Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); - item.Use(deltaTime, Character); + if (damageTarget != null) + { + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } } } + if (damageTarget == null) { return true; } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -2224,20 +2320,11 @@ namespace Barotrauma Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif } - if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (ActiveAttack.CoolDownTimer > 0) { SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f)); - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - float greed = AIParams.AggressionGreed; - if (damageTarget is not Barotrauma.Character) - { - // Halve the greed for attacking non-characters. - greed /= 2; - } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) { @@ -2269,10 +2356,19 @@ namespace Barotrauma private float aimTimer; private float visibilityCheckTimer; private bool canSeeTarget; + private float sinTime; private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) { if (target == null || weapon == null) { return false; } + if (AttackLimb == null) { return false; } + Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; + float dist = toTarget.Length(); Character.CursorPosition = target.WorldPosition; + if (AttackLimb.attack.SwayAmount > 0) + { + sinTime += deltaTime * AttackLimb.attack.SwayFrequency; + Character.CursorPosition += VectorExtensions.Forward(weapon.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2 * AttackLimb.attack.SwayAmount); + } if (Character.Submarine != null) { Character.CursorPosition -= Character.Submarine.Position; @@ -2294,11 +2390,11 @@ namespace Barotrauma aimTimer -= deltaTime; return false; } - Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget); - float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length())); + float minDistance = 300; + float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist)); float margin = MathHelper.PiOver4 * distanceFactor; - if (angle < margin) + if (angle < margin || dist < minDistance) { var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); @@ -2591,13 +2687,19 @@ namespace Barotrauma { // 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 || + { + if (aiTarget.Entity.Submarine.Info.IsWreck || + aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine)) { continue; } + //ignore the megaruin in end levels + if (aiTarget.Entity.Submarine.Info.OutpostGenerationParams != null && + aiTarget.Entity.Submarine.Info.OutpostGenerationParams.ForceToEndLocationIndex > -1) + { + continue; + } } if (aiTarget.Entity is Hull hull) { @@ -2698,7 +2800,7 @@ namespace Barotrauma } else if (CanPassThroughHole(s, i)) { - valueModifier *= isInnerWall ? 1 : 0; + valueModifier *= isInnerWall ? 0.5f : 0; } else if (!canAttackWalls) { @@ -2968,7 +3070,8 @@ namespace Barotrauma // In the attack state allow going into non-allowed zone only when chasing a target. if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; } } - if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) + bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine; + if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _)) { // If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state. bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget); @@ -3588,6 +3691,11 @@ namespace Barotrauma observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f); } reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } protected override void OnStateChanged(AIState from, AIState to) @@ -3609,6 +3717,11 @@ namespace Barotrauma } blockCheckTimer = 0; reachTimer = 0; + sinTime = 0; + if (breakCircling && strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + } } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -3712,6 +3825,7 @@ namespace Barotrauma { targetDir = Vector2.Zero; if (Level.Loaded == null) { return true; } + if (Level.Loaded.LevelData.Biome.IsEndBiome) { return true; } if (AIParams.AvoidAbyss) { if (pos.Y < Level.Loaded.AbyssStart) @@ -3771,6 +3885,7 @@ namespace Barotrauma public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { + IsTryingToSteerThroughGap = true; wallTarget = null; LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 3f7f2ebf3..7f480ca07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -430,7 +430,7 @@ namespace Barotrauma } if (reportProblemsTimer <= 0.0f) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { ReportProblems(); } @@ -444,7 +444,7 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; + bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority); if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) @@ -546,12 +546,11 @@ namespace Barotrauma bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective) { - if (!Character.NeedsAir) { return false; } bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; Hull targetHull = gotoObjective.GetTargetHull(); - return gotoObjective.Target != null && targetHull == null || + return (gotoObjective.Target != null && targetHull == null && !Character.IsImmuneToPressure) || NeedsDivingGear(targetHull, out _) || - insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _))); + (insideSteering && ((PathSteering.CurrentPath.HasOutdoorsNodes && !Character.IsImmuneToPressure) || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)))); } if (isCarrying) @@ -584,7 +583,7 @@ namespace Barotrauma Character.AnimController.InWater || Character.AnimController.HeadInWater || Character.Submarine == null || - (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || + (!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || @@ -621,9 +620,10 @@ namespace Barotrauma } else if (gotoObjective.Mimic) { + bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false); if (!removeSuit) { - removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + removeDivingSuit = !targetHasDivingGear; if (removeDivingSuit) { removeSuit = true; @@ -631,7 +631,7 @@ namespace Barotrauma } if (!removeMask) { - takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + takeMaskOff = !targetHasDivingGear; if (takeMaskOff) { removeMask = true; @@ -783,20 +783,23 @@ namespace Barotrauma private void HandleRelocation(Item item) { - if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC) + if (item.SpawnedInCurrentOutpost) { return; } + if (item.Submarine == null) { return; } + // Only affects bots in the player team + if (!Character.IsOnPlayerTeam) { return; } + // Don't relocate if the item is on a sub of the same team + if (item.Submarine.TeamID == Character.TeamID) { return; } + if (itemsToRelocate.Contains(item)) { return; } + itemsToRelocate.Add(item); + if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) { - if (itemsToRelocate.Contains(item)) { return; } - itemsToRelocate.Add(item); - if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) - { - myPort.OnUnDocked += Relocate; - } - var campaign = GameMain.GameSession.Campaign; - if (campaign != null) - { - // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. - campaign.BeforeLevelLoading += Relocate; - } + myPort.OnUnDocked += Relocate; + } + var campaign = GameMain.GameSession.Campaign; + if (campaign != null) + { + // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. + campaign.BeforeLevelLoading += Relocate; } void Relocate() @@ -989,8 +992,8 @@ namespace Barotrauma targetHull = hull; } } - } - foreach (Item item in Item.ItemList) + } + foreach (Item item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, Character)) @@ -1209,7 +1212,7 @@ namespace Barotrauma } else { - isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0; // Inform other NPCs if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold) { @@ -1523,7 +1526,7 @@ namespace Barotrauma { margin *= 2; } - float minCeilingDist = mainCollider.height / 2 + mainCollider.radius + margin; + float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null; } @@ -1546,23 +1549,19 @@ namespace Barotrauma public bool NeedsDivingGear(Hull hull, out bool needsSuit) { - if (!Character.NeedsAir) - { - needsSuit = false; - return false; - } needsSuit = false; + bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1; if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { - needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - return true; + needsSuit = !Character.IsProtectedFromPressure; + return needsAir || needsSuit; } if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { - return true; + return needsAir; } return false; } @@ -1641,7 +1640,7 @@ namespace Barotrauma { if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || otherCharacter.Info?.Job == null || - !(otherCharacter.AIController is HumanAIController otherHumanAI) || + otherCharacter.AIController is not HumanAIController otherHumanAI || !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) { continue; @@ -1654,7 +1653,7 @@ namespace Barotrauma float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage); - if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); @@ -1745,12 +1744,14 @@ namespace Barotrauma } if (!someoneSpoke) { - if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + if (!item.StolenDuringRound && + Level.Loaded?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) { var reputationLoss = MathHelper.Clamp( (item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice, Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem); - GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss); } item.StolenDuringRound = true; otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); @@ -1843,7 +1844,7 @@ namespace Barotrauma } break; case "reportbrokendevices": - foreach (var item in Item.ItemList) + foreach (var item in Item.RepairableItems) { if (item.CurrentHull != hull) { continue; } if (AIObjectiveRepairItems.IsValidTarget(item, character)) @@ -1924,11 +1925,12 @@ namespace Barotrauma bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { + float hullSafety = character.IsProtectedFromPressure ? 0 : 100; if (isCurrentHull) { - CurrentHullSafety = character.NeedsAir ? 0 : 100; + CurrentHullSafety = hullSafety; } - return CurrentHullSafety; + return hullSafety; } if (isCurrentHull && visibleHulls == null) { @@ -1936,10 +1938,9 @@ namespace Barotrauma visibleHulls = VisibleHulls; } bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); - bool ignoreWater = character.IsProtectedFromPressure(); - bool ignoreOxygen = HasDivingGear(character); + bool ignoreOxygen = HasDivingGear(character); bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.IsCurrentObjective(); - float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); + float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { CurrentHullSafety = safety; @@ -1949,15 +1950,33 @@ namespace Barotrauma private static float CalculateHullSafety(Hull hull, IEnumerable visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) { - if (hull == null) { return character.NeedsAir ? 0 : 100; } - if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; } + bool isProtectedFromPressure = character.IsProtectedFromPressure; + if (hull == null) { return isProtectedFromPressure ? 100 : 0; } + if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; } // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); - float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100); - if (!character.NeedsAir) + float waterFactor = 1; + if (!ignoreWater) + { + if (visibleHulls != null) + { + // Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results. + float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume); + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + else + { + float relativeWaterVolume = hull.WaterVolume / hull.Volume; + waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume); + } + } + if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1) { oxygenFactor = 1; + } + if (isProtectedFromPressure) + { waterFactor = 1; } float fireFactor = 1; @@ -2047,19 +2066,37 @@ namespace Barotrauma bool sameTeam = me.TeamID == other.TeamID; bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other); if (!teamGood) { return false; } - if (!me.IsSameSpeciesOrGroup(other)) { return false; } - if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) - { - var reputation = campaign.Map?.CurrentLocation?.Reputation; - if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) - { - return false; - } - } - if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet) + if (other.IsPet) { // Hostile NPCs are hostile to all pets, unless they are in the same team. - return false; + if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } + } + else + { + if (!me.IsSameSpeciesOrGroup(other)) { return false; } + } + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || + (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) + { + Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; + Identifier npcFaction = npc.Faction; + Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; + if (npcFaction.IsEmpty) + { + //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost + npcFaction = currentLocationFaction; + } + if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) + { + var reputation = campaign.Map?.CurrentLocation?.Reputation; + if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) + { + return false; + } + } + } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5dc043af1..a98233d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -26,7 +26,7 @@ namespace Barotrauma private float findPathTimer; - private const float buttonPressCooldown = 3; + private const float ButtonPressCooldown = 1; private float checkDoorsTimer; private float buttonPressTimer; @@ -96,7 +96,7 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen) { buttonPressTimer = 0; } @@ -211,7 +211,7 @@ namespace Barotrauma currentTarget = target; Vector2 currentPos = host.SimPosition; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; - pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0; + pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) @@ -310,7 +310,7 @@ namespace Barotrauma // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically - if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) + if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius) { diff.Y = 0.0f; } @@ -342,7 +342,7 @@ namespace Barotrauma CheckDoorsInPath(); doorsChecked = true; } - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsFullyOpen) { // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. Reset(); @@ -395,7 +395,7 @@ namespace Barotrauma } //at the same height as the waypoint float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); - float colliderSize = (collider.height / 2 + collider.radius) * 1.25f; + float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; if (heightDiff < colliderSize) { float heightFromFloor = character.AnimController.GetHeightFromFloor(); @@ -510,7 +510,7 @@ namespace Barotrauma private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsBroken) { return true; } - if (!door.IsOpen) + if (door.IsClosed) { if (!door.Item.IsInteractable(character)) { return false; } if (!ShouldBreakDoor(door)) @@ -536,7 +536,7 @@ namespace Barotrauma } foreach (var linked in door.Item.linkedTo) { - if (!(linked is Item linkedItem)) { continue; } + if (linked is not Item linkedItem) { continue; } var button = linkedItem.GetComponent(); if (button == null) { continue; } if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button))) @@ -694,7 +694,7 @@ namespace Barotrauma if (door.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -712,7 +712,7 @@ namespace Barotrauma if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; + buttonPressTimer = shouldBeOpen ? ButtonPressCooldown : 0; } else { @@ -785,7 +785,7 @@ namespace Barotrauma { if (hull.WaterVolume / hull.Rect.Width > 100.0f) { - if (!HumanAIController.HasDivingSuit(character)) + if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1) { penalty += 500.0f; } @@ -808,7 +808,7 @@ namespace Barotrauma private float? GetSingleNodePenalty(PathNode node) { - if (node.Waypoint.isObstructed) { return null; } + if (!node.Waypoint.IsTraversable) { return null; } if (node.IsBlocked()) { return null; } float penalty = 0.0f; if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 5dc813a8c..2dcdc2e10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -244,7 +244,7 @@ namespace Barotrauma else { float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); - float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; + float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs index 3b74d869d..e273488e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -107,7 +107,6 @@ namespace Barotrauma { return MentalType.Normal; } - // test this later int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f)); psychosisIndex = Math.Clamp(psychosisIndex, 0, 4); MentalType mentalType = psychosisIndex switch diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 4ddfe05c1..1c6a1df7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -83,11 +83,16 @@ namespace Barotrauma { if (GameMain.GameSession.RoundDuration < 120.0f && speaker?.CurrentHull != null && + GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f && (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { currentFlags.Add("EnterOutpost".ToIdentifier()); } + if (Level.Loaded.IsEndBiome) + { + currentFlags.Add("EndLevel".ToIdentifier()); + } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { @@ -126,6 +131,10 @@ namespace Barotrauma if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { currentFlags.Add("OutpostNPC".ToIdentifier()); + if (GameMain.GameSession?.Level?.StartLocation?.Faction is Faction faction) + { + currentFlags.Add($"OutpostNPC{faction.Prefab.Identifier}".ToIdentifier()); + } } if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index cb7523470..3a16cf84d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -256,7 +256,9 @@ namespace Barotrauma if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } if (AllowInAnySub) { return true; } if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; } - return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); + return character.Submarine.TeamID == character.TeamID || + character.Submarine.TeamID == character.OriginalTeamID || + character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 3957e400a..5eb1bb55f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -648,11 +648,11 @@ namespace Barotrauma { statusEffects = statusEffects.Concat(hitEffects); } - float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0); + float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0); float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se => { float stunAmount = 0; - var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun"); + var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType); if (stunAffliction != null) { stunAmount = stunAffliction.Strength; @@ -1176,30 +1176,31 @@ namespace Barotrauma if (sqrDistance > repairTool.Range * repairTool.Range) { return; } } float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); - if (pickedBody != null) + // Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay) + var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter); + foreach (var body in pickedBodies) { Character target = null; - if (pickedBody.UserData is Character c) + if (body.UserData is Character c) { target = c; } - else if (pickedBody.UserData is Limb limb) + else if (body.UserData is Limb limb) { target = limb.character; } - if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) + if (target != null && (target != Enemy || HumanAIController.IsFriendly(target))) { - UseWeapon(deltaTime); + return; } } + UseWeapon(deltaTime); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 08d7ea70c..4c1d874d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -200,7 +201,8 @@ namespace Barotrauma (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, - SpeakIfFails = !objectiveManager.IsCurrentOrder() + SpeakIfFails = !objectiveManager.IsCurrentOrder(), + endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 5dcbdb17e..aee20f6ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -19,7 +19,6 @@ namespace Barotrauma private AIObjectiveGetItem getExtinguisherObjective; private AIObjectiveGoTo gotoObjective; - private float useExtinquisherTimer; public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -44,7 +43,8 @@ namespace Barotrauma } else { - float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 3 : 0; float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); @@ -119,24 +119,18 @@ namespace Barotrauma Abandon = true; break; } - float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; - float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); - bool inRange = xDist + yDist < extinguisher.Range; - // Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall. - ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity; - bool move = !inRange || !character.CanSeeTarget(lookTarget); - if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0) + float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X); + float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y); + float dist = xDist + yDist; + bool inRange = dist < extinguisher.Range; + bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull); + bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull)); + bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull)); + if (operateExtinguisher) { - useExtinquisherTimer += deltaTime; - if (useExtinquisherTimer > 2.0f) - { - useExtinquisherTimer = 0.0f; - } - // Aim character.CursorPosition = fs.Position; Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition; - float dist = fromCharacterToFireSource.Length(); - character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2); if (extinguisherItem.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -148,25 +142,29 @@ namespace Barotrauma { character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } + // Prevents running into the flames. + objectiveManager.CurrentObjective.ForceWalk = true; } - if (move) + if (moveCloser) { - //go to the first firesource - if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) - { - DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), - TargetName = fs.Hull.DisplayName - }, - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref gotoObjective))) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f) + { + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), + TargetName = fs.Hull.DisplayName, + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective))) { gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } - else + else if (!operateExtinguisher || isInDamageRange) { - character.AIController.SteeringManager.Reset(); + // Don't walk into the flames. + RemoveSubObjective(ref gotoObjective); + SteeringManager.Reset(); } + // Only target one fire source at the time. break; } } @@ -177,8 +175,20 @@ namespace Barotrauma base.Reset(); getExtinguisherObjective = null; gotoObjective = null; - useExtinquisherTimer = 0; sinTime = 0; + SteeringManager.Reset(); + } + + protected override void OnCompleted() + { + base.OnCompleted(); + SteeringManager.Reset(); + } + + protected override void OnAbandon() + { + base.OnAbandon(); + SteeringManager.Reset(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index e525b613c..3c44882bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -24,7 +24,7 @@ namespace Barotrauma protected override float TargetEvaluation() { if (Targets.None()) { return 0; } - if (!character.IsOnPlayerTeam) { return 100; } + if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } // If there's any security officers onboard, leave fighting for them. @@ -66,7 +66,7 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; } + if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e299a8edb..221067c28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -47,19 +47,12 @@ namespace Barotrauma } if (character.CurrentHull == null) { - if (!character.NeedsAir) - { - Priority = 0; - } - else - { - Priority = ( - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasOrder(o => o.Priority > 0) || - objectiveManager.HasActiveObjective() || - objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) - && HumanAIController.HasDivingSuit(character) ? 0 : 100; - } + Priority = ( + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasOrder(o => o.Priority > 0) || + objectiveManager.HasActiveObjective() || + objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) + && ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; } else { @@ -118,6 +111,11 @@ namespace Barotrauma if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { Priority -= priorityDecrease * deltaTime; + if (currenthullSafety >= 100) + { + // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. + Priority = 0; + } } else { @@ -140,8 +138,8 @@ namespace Barotrauma { if (resetPriority) { return; } var currentHull = character.CurrentHull; + bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); - bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0; if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) { bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit); @@ -221,7 +219,11 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + AllowGoingOutside = + character.IsProtectedFromPressure || + character.CurrentHull == null || + character.CurrentHull.IsTaggedAirlock() || + character.CurrentHull.LeadsOutside(character) }, onCompleted: () => { @@ -352,8 +354,8 @@ namespace Barotrauma //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive //path calculations, only to discard all of them when going through the hulls in the outpost) - float hullSuitability = EstimateHullSuitability(character, hull); - if (!hulls.Any()) + float hullSuitability = EstimateHullSuitability(character, hull); + if (hulls.None()) { hulls.Add(hull); } @@ -448,9 +450,12 @@ namespace Barotrauma { hullSafety = 100; } + float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y; + float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist; // Huge preference for closer targets - float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition); - float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); + float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9604579dd..dd0b1e20b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -155,17 +155,21 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), - onAbandon: () => Abandon = true, - onCompleted: () => + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak) + { + // Use an empty filter to override the default + EndNodeFilter = n => true + }, + onAbandon: () => Abandon = true, + onCompleted: () => + { + if (CheckObjectiveSpecific()) { IsCompleted = true; } + else { - if (CheckObjectiveSpecific()) { IsCompleted = true; } - else - { - // Failed to operate. Probably too far. - Abandon = true; - } - }); + // Failed to operate. Probably too far. + Abandon = true; + } + }); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index b3cdaed85..2f3e75a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -123,6 +123,11 @@ namespace Barotrauma return ignoredTags; } + public static Func CreateEndNodeFilter(ISpatialEntity targetEntity) + { + return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); + } + private bool CheckInventory() { if (IdentifiersOrTags == null) { return false; } @@ -155,11 +160,6 @@ namespace Barotrauma Abandon = true; return; } - if (character.Submarine == null) - { - Abandon = true; - return; - } if (IdentifiersOrTags != null && !isDoneSeeking) { if (checkInventory) @@ -171,9 +171,14 @@ namespace Barotrauma } if (!isDoneSeeking) { + if (character.Submarine == null) + { + Abandon = true; + return; + } if (!AllowDangerousPressure) { - bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0; + bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0); if (dangerousPressure) { #if DEBUG @@ -192,6 +197,11 @@ namespace Barotrauma return; } } + else if (character.Submarine == null) + { + Abandon = true; + return; + } if (targetItem == null || targetItem.Removed) { #if DEBUG @@ -307,7 +317,8 @@ namespace Barotrauma { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, - SpeakIfFails = false + SpeakIfFails = false, + endNodeFilter = CreateEndNodeFilter(moveToTarget) }; }, onAbandon: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 684713e02..e4afa8c54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -33,6 +33,11 @@ namespace Barotrauma public bool DebugLogWhenFails { get; set; } = true; public bool UsePathingOutside { get; set; } = true; + /// + /// Which event action created this objective (if any) + /// + public EventAction SourceEventAction; + public float ExtraDistanceWhileSwimming; public float ExtraDistanceOutsideSub; private float _closeEnoughMultiplier = 1; @@ -45,6 +50,7 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; + private bool cantFindDivingGear; /// /// Display units @@ -85,7 +91,7 @@ namespace Barotrauma /// public bool UseDistanceRelativeToAimSourcePos { get; set; } = false; - public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; @@ -258,48 +264,73 @@ namespace Barotrauma } if (!Abandon) { - if (getDivingGearIfNeeded && !character.LockHands) + if (getDivingGearIfNeeded) { Character followTarget = Target as Character; - bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); - if (Mimic) + bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure; + bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit); + bool tryToGetDivingSuit = needsDivingSuit; + if (Mimic && !character.IsImmuneToPressure) { if (HumanAIController.HasDivingSuit(followTarget)) { - needsDivingGear = true; - needsDivingSuit = true; + tryToGetDivingGear = true; + tryToGetDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget)) + else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1) { - needsDivingGear = true; + tryToGetDivingGear = true; } } bool needsEquipment = false; float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character); - if (needsDivingSuit) + if (tryToGetDivingSuit) { needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } - else if (needsDivingGear) + else if (tryToGetDivingGear) { needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } - if (needsEquipment) + if (character.LockHands) + { + cantFindDivingGear = true; + } + if (cantFindDivingGear && needsDivingSuit) + { + // Don't try to reach the target without a suit because it's lethal. + Abandon = true; + return; + } + if (needsEquipment && !cantFindDivingGear) { SteeringManager.Reset(); - if (findDivingGear != null && !findDivingGear.CanBeCompleted) - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } - else - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); - } + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager), + onAbandon: () => + { + cantFindDivingGear = true; + if (needsDivingSuit) + { + // Shouldn't try to reach the target without a suit, because it's lethal. + Abandon = true; + } + else + { + // Try again without requiring the diving suit + RemoveSubObjective(ref findDivingGear); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), + onAbandon: () => + { + Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null); + RemoveSubObjective(ref findDivingGear); + }, + onCompleted: () => + { + RemoveSubObjective(ref findDivingGear); + }); + } + }, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 72afb8181..3625f7a1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -170,7 +170,8 @@ namespace Barotrauma TargetHull = character.CurrentHull; } - if (behavior == BehaviorType.StayInHull) + bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); + if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) { currentTarget = TargetHull; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; @@ -190,9 +191,6 @@ namespace Barotrauma } else { - bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || - (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); - if (currentTarget != null && !currentTargetIsInvalid) { if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index aed77cb77..06305e134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -23,6 +23,11 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; + /// + /// If undefined, a default filter will be used. + /// + public Func EndNodeFilter; + public bool Override { get; set; } = true; public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); @@ -222,7 +227,7 @@ namespace Barotrauma { target.Item.TryInteract(character, forceSelectKey: true); } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } @@ -232,7 +237,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50) { TargetName = target.Item.Name, - endNodeFilter = node => node.Waypoint.Ladders == null + endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); @@ -290,7 +295,7 @@ namespace Barotrauma } return; } - if (component.AIOperate(deltaTime, character, this)) + if (component.CrewAIOperate(deltaTime, character, this)) { isDoneOperating = completionCondition == null || completionCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index e8b5dea26..9c06fb8b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -17,7 +17,6 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveContainItem refuelObjective; - private float previousCondition = -1; private RepairTool repairTool; private const float WaitTimeBeforeRepair = 0.5f; @@ -196,15 +195,7 @@ namespace Barotrauma Abandon = true; } } - if (previousCondition == -1) - { - previousCondition = Item.Condition; - } - else if (Item.Condition < previousCondition) - { - // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. - Abandon = true; - } + CheckPreviousCondition(deltaTime); } if (Abandon) { @@ -229,7 +220,6 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => { - previousCondition = -1; var objective = new AIObjectiveGoTo(Item, character, objectiveManager) { TargetName = Item.Name @@ -251,6 +241,27 @@ namespace Barotrauma } } + private const float conditionCheckDelay = 1; + private float conditionCheckTimer; + private float previousCondition; + private void CheckPreviousCondition(float deltaTime) + { + if (Item == null || Item.Removed) { return; } + conditionCheckTimer -= deltaTime; + if (conditionCheckTimer > 0) { return; } + conditionCheckTimer = conditionCheckDelay; + if (previousCondition > -1 && Item.Condition < previousCondition) + { + // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. + Abandon = true; + } + else + { + // If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here. + previousCondition = Item.Condition; + } + } + private void FindRepairTool() { foreach (Repairable repairable in Item.Repairables) @@ -303,7 +314,6 @@ namespace Barotrauma base.Reset(); goToObjective = null; refuelObjective = null; - previousCondition = -1; repairTool = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 20be4494f..50f03a240 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -71,11 +71,11 @@ namespace Barotrauma { float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f); vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100; - if (affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { vitality -= affliction.Strength; } - else if (affliction.Prefab.AfflictionType == "poison") + else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType) { vitality -= affliction.Strength; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index c96c97b8c..14fc0d495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,6 +12,9 @@ namespace Barotrauma private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; private bool usingEscapeBehavior, isSteeringThroughGap; + public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 042e913cd..0b48320e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -465,6 +465,10 @@ namespace Barotrauma public readonly OrderTarget TargetPosition; private ISpatialEntity targetSpatialEntity; + + /// + /// Note this property doesn't return the follow target of the Follow objective, as expected! + /// public ISpatialEntity TargetSpatialEntity { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 9e9398673..88db60899 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -348,6 +348,7 @@ namespace Barotrauma { if (body.UserData is Submarine) { return false; } if (body.UserData is Structure s && !s.IsPlatform) { return false; } + if (body.UserData is Voronoi2.VoronoiCell) { return false; } if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs index 7b2abeada..bbd554491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SwarmBehavior.cs @@ -13,6 +13,7 @@ namespace Barotrauma private readonly float minDistFromClosest; private readonly float maxDistFromCenter; private readonly float cohesion; + public bool ForceActive { get; private set; } public List Members { get; private set; } = new List(); public HashSet ActiveMembers { get; private set; } = new HashSet(); @@ -26,9 +27,10 @@ namespace Barotrauma public SwarmBehavior(XElement element, EnemyAIController ai) { this.ai = ai; - minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); - maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); - cohesion = element.GetAttributeFloat("cohesion", 1) / 10; + minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(minDistFromClosest), 10.0f)); + maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat(nameof(maxDistFromCenter), 1000.0f)); + cohesion = element.GetAttributeFloat(nameof(cohesion), 1) / 10; + ForceActive = element.GetAttributeBool(nameof(ForceActive), false); } public static void CreateSwarm(IEnumerable swarm) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index b50108c6d..34092a282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -8,16 +8,90 @@ using System; namespace Barotrauma { - partial class WreckAI : IServerSerializable + internal class SubmarineTurretAI { - public Submarine Wreck { get; private set; } + public Submarine Submarine { get; protected set; } + protected readonly List turrets = new List(); + public Identifier FriendlyTag; + public SubmarineTurretAI(Submarine submarine, Identifier friendlyTag = default) + { + FriendlyTag = friendlyTag; + Submarine = submarine; + foreach (Item item in Item.ItemList) + { + if (item.Submarine != Submarine) { continue; } + var turret = item.GetComponent(); + if (turret != null) + { + turrets.Add(turret); + // Set false, because we manage the turrets in the Update method. + turret.AutoOperate = false; + // Set to full condition, because items don't work when they are broken. + turret.Item.Condition = turret.Item.MaxCondition; + foreach (MapEntity linkedEntity in turret.Item.linkedTo) + { + if (linkedEntity is Item linkedItem) + { + linkedItem.Condition = linkedItem.MaxCondition; + } + } + } + } + LoadAllTurrets(); + } + + public virtual void Update(float deltaTime) + { + if (Submarine == null || Submarine.Removed) { return; } + OperateTurrets(deltaTime, FriendlyTag); + } + + protected virtual void LoadAllTurrets() + { + foreach (var turret in turrets) + { + LoadTurret(turret); + } + } + + protected void LoadTurret(Turret turret, Func ammoFilter = null) + { + foreach (var linkedItem in turret.Item.GetLinkedEntities()) + { + var container = linkedItem.GetComponent(); + if (container == null) { continue; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + if (container.Inventory.GetItemAt(i) != null) { continue; } + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && (ammoFilter == null || ammoFilter(ip)), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) + { + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Submarine); + if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + turret.Item.Remove(); + } + } + } + } + } + + protected void OperateTurrets(float deltaTime, Identifier friendlyTag) + { + foreach (var turret in turrets) + { + turret.UpdateAutoOperate(deltaTime, friendlyTag); + } + } + } + + partial class WreckAI : SubmarineTurretAI, IServerSerializable + { public bool IsAlive { get; private set; } private readonly List allItems; private readonly List thalamusItems; private readonly List thalamusStructures; - private readonly List turrets = new List(); private readonly List wayPoints = new List(); private readonly List hulls = new List(); private readonly List spawnOrgans = new List(); @@ -25,7 +99,7 @@ namespace Barotrauma private bool initialCellsSpawned; - public readonly WreckAIConfig Config; + public WreckAIConfig Config { get; private set; } private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -44,15 +118,10 @@ namespace Barotrauma return wreckAI; } - private WreckAI(Submarine wreck) + private WreckAI(Submarine wreck) : base(wreck) { - Wreck = wreck; - Config = WreckAIConfig.GetRandom(); - if (Config == null) - { - DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); - return; - } + GetConfig(); + if (Config == null) { return; } var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) @@ -60,20 +129,20 @@ namespace Barotrauma DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); return; } - allItems = Wreck.GetItems(false); + allItems = wreck.GetItems(false); thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); - hulls.AddRange(Wreck.GetHulls(false)); + hulls.AddRange(wreck.GetHulls(false)); var potentialBrainHulls = new List<(Hull hull, float weight)>(); - brain = new Item(brainPrefab, Vector2.Zero, Wreck); + brain = new Item(brainPrefab, Vector2.Zero, wreck); thalamusItems.Add(brain); Point minSize = brain.Rect.Size.Multiply(brain.Scale); // Bigger hulls are allowed, but not preferred more than what's sufficent. Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f); // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. - Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(wreck.WorldPosition.ToPoint(), new Point(wreck.Borders.Width - 500, wreck.Borders.Height)); foreach (Hull hull in hulls) { - float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition); + float distanceFromCenter = Vector2.Distance(wreck.WorldPosition, hull.WorldPosition); float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter)); float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width)); float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height)); @@ -121,7 +190,7 @@ namespace Barotrauma var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); if (backgroundPrefab != null) { - new Structure(brainHull.Rect, backgroundPrefab, Wreck); + new Structure(brainHull.Rect, backgroundPrefab, wreck); } var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); if (horizontalWallPrefab != null) @@ -129,8 +198,8 @@ namespace Barotrauma int height = (int)horizontalWallPrefab.Size.Y; int halfHeight = height / 2; int quarterHeight = halfHeight / 2; - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, wreck); } var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); if (verticalWallPrefab != null) @@ -138,50 +207,13 @@ namespace Barotrauma int width = (int)verticalWallPrefab.Size.X; int halfWidth = width / 2; int quarterWidth = halfWidth / 2; - new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); - new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, wreck); } - foreach (Item item in allItems) + foreach (Item item in thalamusItems) { - if (thalamusItems.Contains(item)) - { - // Ensure that thalamus items are visible - item.HiddenInGame = false; - } - else - { - // Load regular turrets - var turret = item.GetComponent(); - if (turret != null) - { - foreach (var linkedItem in item.GetLinkedEntities()) - { - var container = linkedItem.GetComponent(); - if (container == null) { continue; } - for (int i = 0; i < container.Inventory.Capacity; i++) - { - if (container.Inventory.GetItemAt(i) != null) { continue; } - if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && - Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) - { - Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); - if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) - { - item.Remove(); - } - } - } - } - } - } - } - foreach (var item in allItems) - { - var turret = item.GetComponent(); - if (turret != null) - { - turrets.Add(turret); - } + // Ensure that thalamus items are visible + item.HiddenInGame = false; if (item.HasTag(Config.Spawner)) { if (!spawnOrgans.Contains(item)) @@ -195,16 +227,34 @@ namespace Barotrauma } } } - wayPoints.AddRange(Wreck.GetWaypoints(false)); + wayPoints.AddRange(wreck.GetWaypoints(false)); IsAlive = true; - thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); + thalamusStructures = GetThalamusEntities(wreck, Config.Entity).ToList(); + } + + private void GetConfig() + { + Config ??= WreckAIConfig.GetRandom(); + if (Config == null) + { + DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); + } + } + + protected override void LoadAllTurrets() + { + GetConfig(); + foreach (var turret in turrets) + { + LoadTurret(turret, ip => Config.ForbiddenAmmunition.None(id => id == ip.Identifier)); + } } private readonly List destroyedOrgans = new List(); - public void Update(float deltaTime) + public override void Update(float deltaTime) { if (!IsAlive) { return; } - if (Wreck == null || Wreck.Removed) + if (Submarine == null || Submarine.Removed) { Remove(); return; @@ -223,34 +273,60 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); - bool someoneNearby = false; + bool isSomeoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; - foreach (Submarine submarine in Submarine.Loaded) +#if SERVER + foreach (var client in GameMain.Server.ConnectedClients) { - if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + var spectatePos = client.SpectatePos; + if (spectatePos.HasValue) { - someoneNearby = true; - break; + if (IsCloseEnough(spectatePos.Value, minDist)) + { + isSomeoneNearby = true; + break; + } } } - foreach (Character c in Character.CharacterList) +#else + if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist)) { - if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } - if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + isSomeoneNearby = true; + } +#endif + if (!isSomeoneNearby) + { + foreach (Submarine submarine in Submarine.Loaded) { - someoneNearby = true; - break; + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (IsCloseEnough(submarine.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } } } - if (!someoneNearby) { return; } - OperateTurrets(deltaTime); + if (!isSomeoneNearby) + { + foreach (Character c in Character.CharacterList) + { + if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; } + if (IsCloseEnough(c.WorldPosition, minDist)) + { + isSomeoneNearby = true; + break; + } + } + } + if (!isSomeoneNearby) { return; } + OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } + private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist; private void SpawnInitialCells() { @@ -287,7 +363,7 @@ namespace Barotrauma // Snap all tendons foreach (Item item in turret.ActiveProjectiles) { - if (item.GetComponent()?.IsStuckToTarget ?? false) + if (item.GetComponent() is { IsStuckToTarget: true }) { item.Condition = 0; } @@ -314,7 +390,7 @@ namespace Barotrauma { // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. float maxDistance = Sonar.DefaultSonarRange; - if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance) + if (Vector2.DistanceSquared(character.WorldPosition, Submarine.WorldPosition) < maxDistance * maxDistance) { character.Kill(CauseOfDeathType.Unknown, null); } @@ -333,7 +409,7 @@ namespace Barotrauma public void Remove() { Kill(); - RemoveThalamusItems(Wreck); + RemoveThalamusItems(Submarine); thalamusItems?.Clear(); thalamusStructures?.Clear(); } @@ -387,7 +463,7 @@ namespace Barotrauma return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t)); } - void UpdateReinforcements(float deltaTime) + private void UpdateReinforcements(float deltaTime) { if (spawnOrgans.Count == 0) { return; } cellSpawnTimer -= deltaTime; @@ -398,7 +474,7 @@ namespace Barotrauma } } - bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) + private bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) { cell = null; if (protectiveCells.Count >= MaxCellCount) { return false; } @@ -424,19 +500,6 @@ namespace Barotrauma cellSpawnTimer = GetSpawnTime(); return true; } - - void OperateTurrets(float deltaTime) - { - foreach (var turret in turrets) - { - // Never target other creatures than humans with the turrets. - turret.ThalamusOperate(this, deltaTime, - !turret.Item.HasTag("ignorecharacters"), - targetOtherCreatures: false, - !turret.Item.HasTag("ignoresubmarines"), - turret.Item.HasTag("ignoreaimdelay")); - } - } void OnCellDeath(Character character, CauseOfDeath causeOfDeath) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 140aa7c04..1606de078 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -541,11 +541,11 @@ namespace Barotrauma float wobbleStrength = 0.0f; if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType); } if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType); } if (wobbleStrength <= 0.1f) { return 0.0f; } wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 4e3da61fa..ebc92713c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -135,8 +135,14 @@ namespace Barotrauma public override void UpdateAnim(float deltaTime) { - if (Frozen) return; - if (MainLimb == null) { return; } + //wait a bit for the ragdoll to "settle" (for joints to force the limbs to appropriate positions) before starting to animate + if (Timing.TotalTime - character.SpawnTime < 0.1f) { return; } + if (Frozen) { return; } + if (MainLimb == null) + { + ResetState(); + return; + } var mainLimb = MainLimb; levitatingCollider = !IsHanging; @@ -164,6 +170,7 @@ namespace Barotrauma //cannot walk but on dry land -> wiggle around UpdateDying(deltaTime); } + ResetState(); return; } else @@ -176,11 +183,17 @@ namespace Barotrauma { var lowestLimb = FindLowestLimb(); - Collider.SetTransform(new Vector2( - Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), - 0.0f); - + if (InWater) + { + Collider.SetTransform(new Vector2(Collider.SimPosition.X, MainLimb.SimPosition.Y), 0.0f); + } + else + { + Collider.SetTransform(new Vector2( + Collider.SimPosition.X, + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), + 0.0f); + } Collider.Enabled = true; } @@ -223,6 +236,7 @@ namespace Barotrauma if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); + ResetState(); return; } if (character.AnimController.AnimationTestPose) @@ -230,7 +244,11 @@ namespace Barotrauma ApplyTestPose(); } //don't flip when simply physics is enabled - if (SimplePhysicsEnabled) { return; } + if (SimplePhysicsEnabled) + { + ResetState(); + return; + } if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming) { @@ -264,43 +282,47 @@ namespace Barotrauma } } - if (!CurrentFishAnimation.Flip) { return; } - if (IsStuck) { return; } - if (character.AIController != null && !character.AIController.CanFlip) { return; } - - flipCooldown -= deltaTime; - if (TargetDir != Direction.None && TargetDir != dir) + if (!IsStuck && CurrentFishAnimation.Flip && character.AIController is not { CanFlip: false }) { - flipTimer += deltaTime; - // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). - float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; - if (CurrentHull != null) + flipCooldown -= deltaTime; + if (TargetDir != Direction.None && TargetDir != dir) { - // Enemy movement speeds are halved inside submarines - requiredSpeed /= 2; - } - bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; - bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); - if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) - || character.IsRemotePlayer) - { - Flip(); - if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + flipTimer += deltaTime; + // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). + float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; + if (CurrentHull != null) { - Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + // Enemy movement speeds are halved inside submarines + requiredSpeed /= 2; } + bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; + bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); + if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) + || character.IsRemotePlayer) + { + Flip(); + if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) + { + Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); + } + flipTimer = 0.0f; + flipCooldown = CurrentFishAnimation.FlipCooldown; + } + } + else + { flipTimer = 0.0f; - flipCooldown = CurrentFishAnimation.FlipCooldown; } } - else + ResetState(); + + void ResetState() { - flipTimer = 0.0f; + wasAiming = aiming; + aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; } - wasAiming = aiming; - aiming = false; - wasAimingMelee = aimingMelee; - aimingMelee = false; } private bool CanDrag(Character target) @@ -463,19 +485,26 @@ namespace Barotrauma if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; - if (aiming && movement.Length() <= 0.1f) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } - - if (!isMoving) + if (!isMoving && !CurrentSwimParams.UpdateAnimationWhenNotMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; + if (aiming) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; + TargetMovement = new Vector2(0.0f, -0.1f); + float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver2 * Dir); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier * 2); + if (TorsoAngle.HasValue) + { + Limb torso = GetLimb(LimbType.Torso); + if (torso != null) + { + SmoothRotateWithoutWrapping(torso, newRotation + TorsoAngle.Value * Dir, mainLimb, TorsoTorque * 2); + } + } + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 8c67c1fd7..bc646d3dd 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; + float shoulderHeight = Collider.Height / 2.0f; if (inWater) { shoulderHeight += 0.4f; @@ -308,7 +308,7 @@ namespace Barotrauma Collider.SetTransform(new Vector2( Collider.SimPosition.X, - Math.Max(lowestLimb.SimPosition.Y + (Collider.radius + Collider.height / 2), Collider.SimPosition.Y)), + Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)), Collider.Rotation); Collider.FarseerBody.ResetDynamics(); @@ -459,7 +459,8 @@ namespace Barotrauma void UpdateStanding() { - if (CurrentGroundedParams == null) { return; } + var currentGroundedParams = CurrentGroundedParams; + if (currentGroundedParams == null) { return; } Vector2 handPos; Limb leftFoot = GetLimb(LimbType.LeftFoot); @@ -482,7 +483,7 @@ namespace Barotrauma walkCycleMultiplier *= 1.5f; } - float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale; + float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale; Vector2 colliderPos = GetColliderBottom(); if (Math.Abs(TargetMovement.X) > 1.0f) @@ -583,7 +584,7 @@ namespace Barotrauma } float stepLift = TargetMovement.X == 0.0f ? 0 : - (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + (float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; @@ -598,7 +599,7 @@ namespace Barotrauma if (!head.Disabled) { - y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier; + y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier; if (HeadPosition.HasValue) { y += HeadPosition.Value; } if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; } head.PullJointWorldAnchorB = @@ -615,18 +616,18 @@ namespace Barotrauma if (TorsoAngle.HasValue && !torso.Disabled) { float torsoAngle = TorsoAngle.Value; - float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); + float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType); if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque); } if (!head.Disabled) { - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque); } else { @@ -665,16 +666,16 @@ namespace Barotrauma if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged - float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); + float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); - if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + if (currentGroundedParams.FootLiftHorizontalFactor > 0) { // Calculate the foot y dynamically based on the foot position relative to the waist, // so that the foot aims higher when it's behind the waist and lower when it's in the front. float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; - float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); - float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor); float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); footPos.Y *= xFactor; } @@ -698,19 +699,19 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = colliderPos + footPos; - MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); + MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } //calculate the positions of hands handPos = torso.SimPosition; - handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X; + handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X; - float lowerY = CurrentGroundedParams.HandClampY; + float lowerY = currentGroundedParams.HandClampY; - handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y)); + handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y)); Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y); @@ -718,13 +719,13 @@ namespace Barotrauma { HandIK(rightHand, torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } if (leftHand != null && !leftHand.Disabled) { HandIK(leftHand, torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), - CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); + currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength); } } else @@ -755,8 +756,8 @@ namespace Barotrauma { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; - float footMoveForce = CurrentGroundedParams.FootMoveStrength; - float legBendTorque = CurrentGroundedParams.LegBendTorque; + float footMoveForce = currentGroundedParams.FootMoveStrength; + float legBendTorque = currentGroundedParams.LegBendTorque; if (Crouching) { // Keeps the pose @@ -764,7 +765,7 @@ namespace Barotrauma footMoveForce *= 2; } MoveLimb(foot, footPos, footMoveForce); - FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); + FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians); } } @@ -780,7 +781,7 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength); } //get the elbow to a neutral rotation @@ -791,14 +792,14 @@ namespace Barotrauma if (elbow != null) { float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); - forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength); } } // Try to keep the wrist straight LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type); if (wrist != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength); + hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength); } } } @@ -1130,7 +1131,7 @@ namespace Barotrauma ladderSimPos -= currentHull.Submarine.SimPosition; } - float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; + float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f; float torsoPos = TorsoPosition ?? 0; MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); float headPos = HeadPosition ?? 0; @@ -1225,7 +1226,7 @@ namespace Barotrauma if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); } //reached the bottom -> can't go further down - float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.height; + float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height; if (floorFixture != null && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) && !floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) && diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index feeef3201..873ca5a6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -174,18 +174,18 @@ namespace Barotrauma if (value == colliderIndex || collider == null) { return; } if (value >= collider.Count || value < 0) { return; } - if (collider[colliderIndex].height < collider[value].height) + if (collider[colliderIndex].Height < collider[value].Height) { Vector2 pos1 = collider[colliderIndex].SimPosition; - pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor; + pos1.Y -= collider[colliderIndex].Height * ColliderHeightFromFloor; Vector2 pos2 = pos1; - pos2.Y += collider[value].height * 1.1f; + pos2.Y += collider[value].Height * 1.1f; if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; } } Vector2 pos = collider[colliderIndex].SimPosition; - pos.Y -= collider[colliderIndex].height * 0.5f; - pos.Y += collider[value].height * 0.5f; + pos.Y -= collider[colliderIndex].Height * 0.5f; + pos.Y += collider[value].Height * 0.5f; collider[value].SetTransform(pos, collider[colliderIndex].Rotation); collider[value].LinearVelocity = collider[colliderIndex].LinearVelocity; @@ -576,6 +576,10 @@ namespace Barotrauma protected void AddLimb(LimbParams limbParams) { + if (limbParams.ID < 0 || limbParams.ID > 255) + { + throw new Exception($"Invalid limb params in limb \"{limbParams.Type}\". \"{limbParams.ID}\" is not a valid limb ID."); + } byte ID = Convert.ToByte(limbParams.ID); Limb limb = new Limb(this, character, limbParams); limb.body.FarseerBody.OnCollision += OnLimbCollision; @@ -681,6 +685,10 @@ namespace Barotrauma } return true; } + else if (character.Submarine != null && structure.Submarine != null && character.Submarine != structure.Submarine) + { + return false; + } Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) @@ -1212,7 +1220,9 @@ namespace Barotrauma RefreshFloorY(ignoreStairs: Stairs == null); if (currentHull.WaterPercentage > 0.001f) { - float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY()); + (float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY(); + float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits); + float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); @@ -1222,13 +1232,21 @@ namespace Barotrauma { //set floorY to the position of the floor in the hull below the character var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false); - if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + if (lowerHull != null) + { + floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); + } } } float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; - if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f) + if (Collider.SimPosition.Y < waterSurfaceY) { - inWater = true; + //too deep to stand up, or not enough room to stand up + if (waterSurfaceY - floorY > standHeight * 0.8f || + ceilingY - floorY < standHeight * 0.8f) + { + inWater = true; + } } } } @@ -1291,7 +1309,7 @@ namespace Barotrauma if (!inWater && character.AllowInput && levitatingCollider && Collider.LinearVelocity.Y > -ImpactTolerance && onGround) { - float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.height * 0.5f) + Collider.radius + ColliderHeightFromFloor; + float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor; if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f && onGround) { if (Stairs != null) @@ -1624,7 +1642,7 @@ namespace Barotrauma { floorFixture = standOnFloorFixture; standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; - if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) + if (rayStart.Y - standOnFloorY < Collider.Height * 0.5f + Collider.Radius + ColliderHeightFromFloor * 1.2f) { onGround = true; if (standOnFloorFixture.CollisionCategories == Physics.CollisionStairs) @@ -1663,22 +1681,34 @@ namespace Barotrauma } } + /// + /// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in) + /// public float GetSurfaceY() + { + return GetWaterSurfaceAndCeilingY().WaterSurfaceY; + } + + /// + /// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in). + /// + private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY() { //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside if (currentHull == null || character.CurrentHull == null) { - return float.PositiveInfinity; + return (float.PositiveInfinity, float.PositiveInfinity); } - - float surfacePos = currentHull.Surface; + + float surfaceY = currentHull.Surface; + float ceilingY = currentHull.Rect.Y; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); //if the hull is almost full of water, check if there's a water-filled hull above it //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) - { - GetSurfacePos(currentHull, ref surfacePos); - void GetSurfacePos(Hull hull, ref float prevSurfacePos) + { + GetSurfacePos(currentHull, ref surfaceY, ref ceilingY); + void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos) { if (prevSurfacePos > surfaceThreshold) { return; } foreach (Gap gap in hull.ConnectedGaps) @@ -1689,6 +1719,7 @@ namespace Barotrauma //if the gap is above us and leads outside, there's no surface to limit the movement if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) { + ceilingPos += 100000.0f; prevSurfacePos += 100000.0f; return; } @@ -1697,15 +1728,16 @@ namespace Barotrauma { if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { - prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); - GetSurfacePos(otherHull, ref prevSurfacePos); + prevSurfacePos = Math.Max(surfaceY, otherHull.Surface); + ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y); + GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos); break; } } } } } - return surfacePos; + return (surfaceY, ceilingY); } public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) @@ -1814,7 +1846,7 @@ namespace Barotrauma protected void CheckDistFromCollider() { - float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + float allowedDist = Math.Max(Math.Max(Collider.Radius, Collider.Width), Collider.Height) * 2.0f; allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 702512ca4..3b4bc3f15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -181,13 +181,13 @@ namespace Barotrauma [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, IsPropertySaveable.Yes)] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool Ranged { get; set; } - [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")] + [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, IsPropertySaveable.Yes)] + [Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable] public float RequiredAngle { get; set; } [Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable] @@ -199,6 +199,12 @@ namespace Barotrauma [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable] public int RotationLimbIndex { get; set; } + [Serialize(0f, IsPropertySaveable.Yes, description:"How much the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items). Default 0 means the weapon is not swayed at all."), Editable] + public float SwayAmount { get; set; } + + [Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable] + public float SwayFrequency { get; set; } + /// /// Legacy support. Use Afflictions. /// @@ -337,9 +343,10 @@ namespace Barotrauma return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime; } - public float GetItemDamage(float deltaTime) + public float GetItemDamage(float deltaTime, float multiplier = 1) { - return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime; + float dmg = ItemDamage * multiplier; + return (Duration == 0.0f) ? dmg : dmg * deltaTime; } public float GetTotalDamage(bool includeStructureDamage = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index b5a8c864d..cdf2ea7e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -139,6 +139,12 @@ namespace Barotrauma public bool IsEscorted { get; set; } public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty; + public bool DoesBleed + { + get => Params.Health.DoesBleed; + set => Params.Health.DoesBleed = value; + } + public readonly Dictionary Properties; public Dictionary SerializableProperties { @@ -176,6 +182,13 @@ namespace Barotrauma } } + private Identifier? faction; + public Identifier Faction + { + get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; } + set { faction = value; } + } + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -187,6 +200,13 @@ namespace Barotrauma } } + + private CharacterTeamType? originalTeamID; + public CharacterTeamType OriginalTeamID + { + get { return originalTeamID ?? teamID; } + } + private Wallet wallet; public Wallet Wallet @@ -208,7 +228,7 @@ namespace Barotrauma protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; - const string OriginalTeamIdentifier = "original"; + private const string OriginalChangeTeamIdentifier = "original"; private void ThrowIfAccessingWalletsInSingleplayer() { @@ -223,20 +243,16 @@ namespace Barotrauma public void SetOriginalTeam(CharacterTeamType newTeam) { - TryRemoveTeamChange(OriginalTeamIdentifier); + TryRemoveTeamChange(OriginalChangeTeamIdentifier); currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base); - TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange); + TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange); } - protected void ChangeTeam(CharacterTeamType newTeam) + private void ChangeTeam(CharacterTeamType newTeam) { - if (newTeam == teamID) - { - return; - } - teamID = newTeam; - if (info != null) { info.TeamID = newTeam; } - + if (newTeam == teamID) { return; } + if (originalTeamID == null) { originalTeamID = teamID; } + TeamID = newTeam; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -280,7 +296,7 @@ namespace Barotrauma { if (currentTeamChange == removedTeamChange) { - currentTeamChange = activeTeamChanges[OriginalTeamIdentifier]; + currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier]; } } return activeTeamChanges.Remove(identifier); @@ -314,7 +330,9 @@ namespace Barotrauma } } - public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2; + + public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2; public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; @@ -363,7 +381,7 @@ namespace Barotrauma public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); - public Identifier Group => Params.Group; + public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group; public bool IsHumanoid => Params.Humanoid; @@ -461,10 +479,15 @@ namespace Barotrauma } set { - if (info != null && info != value) info.Remove(); - + if (info != null && info != value) + { + info.Remove(); + } info = value; - if (info != null) info.Character = this; + if (info != null) + { + info.Character = this; + } } } @@ -524,8 +547,13 @@ namespace Barotrauma } set { + bool wasHidden = HideFace; hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); - if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true); + bool isHidden = HideFace; + if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden) + { + info.CheckDisguiseStatus(true); + } } } @@ -755,7 +783,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } - return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength); } } @@ -839,7 +867,7 @@ namespace Barotrauma public float Bleeding { - get { return CharacterHealth.GetAfflictionStrength("bleeding", true); } + get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); } } private bool speechImpedimentSet; @@ -1044,7 +1072,7 @@ namespace Barotrauma public bool InWater => AnimController is AnimController { InWater: true }; - public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold; + public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100; public bool GodMode = false; @@ -1102,6 +1130,12 @@ namespace Barotrauma public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; + public float AITurretPriority + { + get => Params.AITurretPriority; + private set => Params.AITurretPriority = value; + } + public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); public OnDeathHandler OnDeath; @@ -1625,7 +1659,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\"."); } - else if (humanPrefab.GiveItems(this, Submarine)) + else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint)) { return; } @@ -1698,7 +1732,7 @@ namespace Barotrauma if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) { skillLevel += skillValue; - break; + break; } } @@ -1707,9 +1741,7 @@ namespace Barotrauma } skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); - - - return skillLevel; + return Math.Max(skillLevel, 0); } // TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner @@ -1907,7 +1939,7 @@ namespace Barotrauma { if (limb != null) { - sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage")); + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType)); } return Math.Clamp(sum, 0, 1f); } @@ -2203,24 +2235,7 @@ namespace Barotrauma if (SelectedItem != null) { - if (IsKeyDown(InputType.Aim) || !SelectedItem.RequireAimToSecondaryUse) - { - SelectedItem.SecondaryUse(deltaTime, this); - } - if (IsKeyDown(InputType.Use) && SelectedItem != null && !SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } - if (IsKeyDown(InputType.Shoot) && SelectedItem != null && SelectedItem.IsShootable) - { - if (!SelectedItem.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - SelectedItem.Use(deltaTime, this); - } - } + tryUseItem(SelectedItem, deltaTime); } if (SelectedCharacter != null) @@ -3036,7 +3051,7 @@ namespace Barotrauma foreach (Character character in GameMain.LuaCs.Game.UpdatePriorityCharacters) { - if (character.Removed) continue; + if (character.Removed) { continue; } character.Update(deltaTime, cam); } @@ -3129,8 +3144,7 @@ namespace Barotrauma if (NeedsAir) { //implode if not protected from pressure, and either outside or in a high-pressure hull - if (!IsProtectedFromPressure() && - (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) + if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)) { if (CharacterHealth.PressureKillDelay <= 0.0f) { @@ -3157,15 +3171,17 @@ namespace Barotrauma PressureTimer = 0.0f; } } - else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && - PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) && - WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure)) + else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure) { - //implode if below crush depth, and either outside or in a high-pressure hull - if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f; + if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth) { - Implode(); - if (IsDead) { return; } + //implode if below crush depth, and either outside or in a high-pressure hull + if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f) + { + Implode(); + if (IsDead) { return; } + } } } @@ -4085,17 +4101,7 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } - CreatureMetrics.Instance.Killed.Add(target.SpeciesName); - AddEncounter(target); - } - - public void AddEncounter(Character other) - { - if (!IsOnPlayerTeam) { return; } - if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } - CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); - CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); + CreatureMetrics.RecordKill(target.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -4130,13 +4136,6 @@ namespace Barotrauma } } -#if CLIENT - if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) - { - CharacterHUD.ShowBossHealthBar(this); - } -#endif - Vector2 dir = hitLimb.WorldPosition - worldPosition; if (Math.Abs(attackImpulse) > 0.0f) { @@ -4178,13 +4177,24 @@ namespace Barotrauma if (attacker != null && attacker != this && !attacker.Removed) { AddAttacker(attacker, attackResult.Damage); - AddEncounter(attacker); - attacker.AddEncounter(this); + if (IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(attacker.SpeciesName); + } + if (attacker.IsOnPlayerTeam) + { + CreatureMetrics.AddEncounter(SpeciesName); + } } ApplyStatusEffects(ActionType.OnDamaged, 1.0f); hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); } - +#if CLIENT + if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID) + { + CharacterHUD.ShowBossHealthBar(this, attackResult.Damage); + } +#endif return attackResult; } @@ -4202,7 +4212,8 @@ namespace Barotrauma { if (affliction.Prefab.IsBuff) { continue; } if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; } - if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || + affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType) { if (!Params.Health.PoisonImmunity) { @@ -4282,7 +4293,7 @@ namespace Barotrauma if (Screen.Selected != GameMain.GameScreen) { return; } if (newStun > 0 && Params.Health.StunImmunity) { - if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) + if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } @@ -4309,7 +4320,7 @@ namespace Barotrauma float eatingRegen = Params.Health.HealthRegenerationWhenEating; if (eatingRegen > 0) { - CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime); + CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime); } } if (statusEffects.TryGetValue(actionType, out var statusEffectList)) @@ -4538,6 +4549,9 @@ namespace Barotrauma { foreach (Item heldItem in HeldItems.ToList()) { + //if the item is both wearable and holdable, and currently worn, don't drop the item + var wearable = heldItem.GetComponent(); + if (wearable is { IsActive: true }) { continue; } heldItem.Drop(this); } } @@ -4678,6 +4692,7 @@ namespace Barotrauma Submarine = null; AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false); AnimController.FindHull(worldPos, setSubmarine: true); + CurrentHull = AnimController.CurrentHull; if (AIController is HumanAIController humanAI) { humanAI.PathSteering?.ResetPath(); @@ -4924,34 +4939,36 @@ namespace Barotrauma return visibleHulls; } - public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos); + + public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null) { - Vector2 targetPos = target.SimPosition; + Vector2 targetPos = to.SimPosition; if (worldPos.HasValue) { Vector2 wp = worldPos.Value; - if (target.Submarine != null) + if (to.Submarine != null) { - wp -= target.Submarine.Position; + wp -= to.Submarine.Position; } targetPos = ConvertUnits.ToSimUnits(wp); } - if (Submarine == null && target.Submarine != null) + if (from.Submarine == null && to.Submarine != null) { // outside and targeting inside - targetPos += target.Submarine.SimPosition; + targetPos += to.Submarine.SimPosition; } - else if (Submarine != null && target.Submarine == null) + else if (from.Submarine != null && to.Submarine == null) { // inside and targeting outside - targetPos -= Submarine.SimPosition; + targetPos -= from.Submarine.SimPosition; } - else if (Submarine != target.Submarine) + else if (from.Submarine != to.Submarine) { - if (Submarine != null && target.Submarine != null) + if (from.Submarine != null && to.Submarine != null) { // both inside, but in different subs - Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition; + Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition; targetPos -= diff; } } @@ -4973,13 +4990,14 @@ namespace Barotrauma public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier; - public bool IsProtectedFromPressure() - { - return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - } + /// + /// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit). + /// + public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); - // Talent logic begins here. Should be encapsulated to its own controller soon + public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure); + #region Talents private readonly List characterTalents = new List(); public void LoadTalents() @@ -5053,6 +5071,49 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (!talentOption.HasMaxTalents(info.UnlockedTalents)) + { + return false; + } + } + } + } + return true; + } + + public bool HasTalents() + { + return characterTalents.Any(); + } + + public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, abilityObject); + } + } + + public void CheckTalents(AbilityEffectType abilityEffectType) + { + foreach (var characterTalent in characterTalents) + { + characterTalent.CheckTalent(abilityEffectType, null); + } + } + + partial void OnTalentGiven(TalentPrefab talentPrefab); + + #endregion + private readonly HashSet sameRoomHulls = new(); /// @@ -5079,24 +5140,6 @@ namespace Barotrauma return sameRoomHulls.Contains(character.CurrentHull); } - public bool HasUnlockedAllTalents() - { - if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (!talentOption.HasMaxTalents(info.UnlockedTalents)) - { - return false; - } - } - } - } - return true; - } - public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -5106,27 +5149,6 @@ namespace Barotrauma return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead); } - public bool HasTalents() - { - return characterTalents.Any(); - } - - public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, abilityObject); - } - } - - public void CheckTalents(AbilityEffectType abilityEffectType) - { - foreach (var characterTalent in characterTalents) - { - characterTalent.CheckTalent(abilityEffectType, null); - } - } - public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); @@ -5190,7 +5212,6 @@ namespace Barotrauma #endif partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -5366,7 +5387,7 @@ namespace Barotrauma public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other); - public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group); public void StopClimbing() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 49c273024..58daaa291 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -315,6 +315,8 @@ namespace Barotrauma public HashSet UnlockedTalents { get; private set; } = new HashSet(); + public (Identifier factionId, float reputation) MinReputationToHire; + /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection /// @@ -508,8 +510,11 @@ namespace Barotrauma public List CurrentOrders { get; } = new List(); - //unique ID given to character infos in MP - //used by clients to identify which infos are the same to prevent duplicate characters in round summary + + /// + /// Unique ID given to character infos in MP. Non-persistent. + /// Used by clients to identify which infos are the same to prevent duplicate characters in round summary. + /// public ushort ID; public List SpriteTags @@ -667,7 +672,6 @@ namespace Barotrauma { Name = GetRandomName(randSync); } - TryLoadNameAndTitle(npcIdentifier); SetPersonalityTrait(); @@ -824,6 +828,8 @@ namespace Barotrauma MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); UnlockedTalents = new HashSet(); + MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f)); + foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; @@ -919,17 +925,25 @@ namespace Barotrauma } } + /// + /// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifier() { - return GetIdentifier(Name); + return GetIdentifierHash(Name); } + /// + /// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job. + /// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique. + /// public int GetIdentifierUsingOriginalName() { - return GetIdentifier(OriginalName); + return GetIdentifierHash(OriginalName); } - private int GetIdentifier(string name) + private int GetIdentifierHash(string name) { int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s))); id ^= Head.HairIndex << 12; @@ -1152,7 +1166,7 @@ namespace Barotrauma partial void LoadAttachmentSprites(); - private int CalculateSalary() + public int CalculateSalary() { if (Name == null || Job == null) { return 0; } @@ -1394,6 +1408,13 @@ namespace Barotrauma charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); + if (MinReputationToHire.factionId != default) + { + charElement.Add( + new XAttribute("factionId", Name), + new XAttribute("minreputation", MinReputationToHire.reputation)); + } + if (Character != null) { if (Character.AnimController.CurrentHull != null) @@ -1502,7 +1523,7 @@ namespace Barotrauma } if (order.OrderGiver != null) { - orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID)); + orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier())); } if (order.TargetSpatialEntity?.Submarine is Submarine targetSub) { @@ -1596,8 +1617,8 @@ namespace Barotrauma continue; } var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); - int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1); - var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null; + int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1); + var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null; Entity targetEntity = null; switch (targetType) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index cb1b33763..d38730699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; -using static Barotrauma.CharacterInfo; namespace Barotrauma { @@ -19,6 +17,8 @@ namespace Barotrauma public string Name => Identifier.Value; public Identifier VariantOf { get; } + public CharacterPrefab ParentPrefab { get; set; } + public void InheritFrom(CharacterPrefab parent) { ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); @@ -38,7 +38,7 @@ namespace Barotrauma } } - private XElement originalElement; + private readonly XElement originalElement; public ContentXElement ConfigElement { get; private set; } public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } @@ -49,10 +49,6 @@ namespace Barotrauma public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); - /// - /// Searches for a character config file from all currently selected content packages, - /// or from a specific package if the contentPackage parameter is given. - /// public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { if (!Prefabs.ContainsKey(speciesName)) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 6b5088913..5d5f5b128 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -19,6 +19,10 @@ namespace Barotrauma private float fluctuationTimer; + private AfflictionPrefab.Effect activeEffect; + private float prevActiveEffectStrength; + protected bool activeEffectDirty = true; + protected float _strength; [Serialize(0f, IsPropertySaveable.Yes), Editable] @@ -46,6 +50,7 @@ namespace Barotrauma Duration = Prefab.Duration; } _strength = newValue; + activeEffectDirty = true; } } @@ -147,7 +152,16 @@ namespace Barotrauma MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; } - public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); + public AfflictionPrefab.Effect GetActiveEffect() + { + if (activeEffectDirty) + { + activeEffect = Prefab.GetActiveEffect(_strength); + prevActiveEffectStrength = _strength; + activeEffectDirty = false; + } + return activeEffect; + } public float GetVitalityDecrease(CharacterHealth characterHealth) { @@ -158,7 +172,7 @@ namespace Barotrauma { if (strength < Prefab.ActivationThreshold) { return 0.0f; } strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength); - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } @@ -349,7 +363,7 @@ namespace Barotrauma public float GetStatValue(StatTypes statType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; } if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value)) { @@ -363,7 +377,7 @@ namespace Barotrauma public bool HasFlag(AbilityFlags flagType) { - if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; } return currentEffect.AfflictionAbilityFlags.HasFlag(flagType); } @@ -415,6 +429,7 @@ namespace Barotrauma } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); + activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { @@ -445,7 +460,10 @@ namespace Barotrauma var currentEffect = GetActiveEffect(); if (currentEffect != null) { - currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb)); + foreach (var statusEffect in currentEffect.StatusEffects) + { + ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb); + } } } @@ -484,6 +502,7 @@ namespace Barotrauma { _nonClampedStrength = strength; _strength = _nonClampedStrength; + activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength); } public bool ShouldShowIcon(Character afflictedCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index b72ed15c1..724273ad6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -22,7 +22,7 @@ namespace Barotrauma private Character character; - private bool stun = true; + private bool stun = false; private readonly List huskInfection = new List(); @@ -43,6 +43,7 @@ namespace Barotrauma DeactivateHusk(); highestStrength = 0; } + activeEffectDirty = true; } } private float highestStrength; @@ -216,7 +217,8 @@ namespace Barotrauma private void DeactivateHusk() { if (character?.AnimController == null || character.Removed) { return; } - if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) + if (Prefab is AfflictionPrefabHusk { NeedsAir: false } && + !character.CharacterHealth.GetAllAfflictions().Any(a => a != this && a.Prefab is AfflictionPrefabHusk { NeedsAir: false })) { character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 997d13a25..be5da631e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -68,7 +68,6 @@ namespace Barotrauma } // Remove "[speciesname]" for backward support (we don't use it anymore) HuskedSpeciesName = HuskedSpeciesName.Remove("[speciesname]").ToIdentifier(); - TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); @@ -110,7 +109,6 @@ namespace Barotrauma public float TransformThresholdOnDeath; public readonly Identifier HuskedSpeciesName; - public readonly Identifier[] TargetSpecies; public readonly bool TransferBuffs; public readonly bool SendMessages; @@ -344,17 +342,30 @@ namespace Barotrauma } } + public static readonly Identifier DamageType = "damage".ToIdentifier(); + public static readonly Identifier BurnType = "burn".ToIdentifier(); + public static readonly Identifier BleedingType = "bleeding".ToIdentifier(); + public static readonly Identifier ParalysisType = "paralysis".ToIdentifier(); + public static readonly Identifier PoisonType = "poison".ToIdentifier(); + public static readonly Identifier StunType = "stun".ToIdentifier(); + public static readonly Identifier EMPType = "emp".ToIdentifier(); + public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier(); + public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier(); + public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier(); + public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier(); + public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"]; public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; - public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; - public static AfflictionPrefab Burn => Prefabs["burn"]; + public static AfflictionPrefab Bleeding => Prefabs[BleedingType]; + public static AfflictionPrefab Burn => Prefabs[BurnType]; public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; public static AfflictionPrefab Pressure => Prefabs["pressure"]; - public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab Stun => Prefabs[StunType]; public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public override void Dispose() { } @@ -421,6 +432,9 @@ namespace Barotrauma public readonly float BurnOverlayAlpha; public readonly float DamageOverlayAlpha; + //steam achievement given when the controlled character receives the affliction + public readonly Identifier AchievementOnReceived; + //steam achievement given when the affliction is removed from the controlled character public readonly Identifier AchievementOnRemoved; @@ -453,6 +467,8 @@ namespace Barotrauma private readonly ConstructorInfo constructor; + public Identifier[] TargetSpecies { get; protected set; } + public readonly bool ResetBetweenRounds; public IEnumerable> TreatmentSuitability @@ -536,13 +552,22 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f); - CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); - SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); + CauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", ""))) + .Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = + TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}") + .Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", ""))) + .Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray(nameof(IconColors), null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnReceived = element.GetAttributeIdentifier(nameof(AchievementOnReceived), ""); AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); + ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 55b3a1d09..ede95e88b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -344,12 +344,12 @@ namespace Barotrauma return null; } - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + public T GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction { return GetAffliction(identifier, allowLimbAfflictions) as T; } - public Affliction GetAffliction(string identifier, Limb limb) + public Affliction GetAffliction(Identifier identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { @@ -385,7 +385,7 @@ namespace Barotrauma /// The limb the affliction is attached to /// Does the affliction have to be attached to only the specific limb. /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. - public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) + public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific) { if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } @@ -406,7 +406,7 @@ namespace Barotrauma return strength; } - public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true) + public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true) { float strength = 0.0f; foreach (KeyValuePair kvp in afflictions) @@ -489,16 +489,19 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } - + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(afflictions.Keys); - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + foreach (var affliction in afflictions) + { + if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) + { + matchingAfflictions.Add(affliction.Key); + } + } + ReduceMatchingAfflictions(amount, treatmentAction); } @@ -515,18 +518,21 @@ namespace Barotrauma ReduceMatchingAfflictions(amount, treatmentAction); } - public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null) { - if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); } if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } - + matchingAfflictions.Clear(); - matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); - - matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier != affliction && - a.Prefab.AfflictionType != affliction); - + var targetLimbHealth = limbHealths[targetLimb.HealthIndex]; + foreach (var affliction in afflictions) + { + if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) && + affliction.Value == targetLimbHealth) + { + matchingAfflictions.Add(affliction.Key); + } + } ReduceMatchingAfflictions(amount, treatmentAction); } @@ -633,7 +639,7 @@ namespace Barotrauma KillIfOutOfVitality(); } - public float GetLimbDamage(Limb limb, string afflictionType = null) + public float GetLimbDamage(Limb limb, Identifier afflictionType) { float damageStrength; if (limb.IsSevered) @@ -646,16 +652,16 @@ namespace Barotrauma // Therefore with e.g. 80 health, the max damage per limb would be 40. // Having at least 40 damage on both legs would cause maximum limping. float max = MaxVitality / 2; - if (string.IsNullOrEmpty(afflictionType)) + if (afflictionType.IsEmpty) { - float damage = GetAfflictionStrength("damage", limb, true); - float bleeding = GetAfflictionStrength("bleeding", limb, true); - float burn = GetAfflictionStrength("burn", limb, true); + float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true); + float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true); + float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true); damageStrength = Math.Min(damage + bleeding + burn, max); } else { - damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max); + damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max); } return damageStrength / max; } @@ -712,22 +718,17 @@ namespace Barotrauma if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; } if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } - if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType) { - if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0) - { - return; - } - } - if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; } - if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; } - if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) - { - if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) + if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0) { return; } } + if (Character.Params.Health.PoisonImmunity && + (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; } + if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; } + if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } var should = GameMain.LuaCs.Hook.Call("character.applyAffliction", this, limbHealth, newAffliction, allowStacking); @@ -769,7 +770,9 @@ namespace Barotrauma Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); afflictions.Add(copyAffliction, limbHealth); - + SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character); + MedicalClinic.OnAfflictionCountChanged(Character); + Character.HealthUpdateInterval = 0.0f; CalculateVitality(); @@ -842,10 +845,16 @@ namespace Barotrauma } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } + foreach (var affliction in afflictionsToRemove) { afflictions.Remove(affliction); - } + } + + if (afflictionsToRemove.Count is not 0) + { + MedicalClinic.OnAfflictionCountChanged(Character); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); @@ -899,6 +908,11 @@ namespace Barotrauma } } + /// + /// 0-1. + /// + public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab); + private void UpdateOxygen(float deltaTime) { if (!Character.NeedsOxygen) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e3d7bedf4..18ff8ac7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -27,6 +27,32 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float AimAccuracy { get; protected set; } + [Serialize(1f, IsPropertySaveable.No)] + public float SkillMultiplier { get; protected set; } + + [Serialize(0, IsPropertySaveable.No)] + public int ExperiencePoints { get; private set; } + + private readonly HashSet tags = new HashSet(); + + [Serialize("", IsPropertySaveable.Yes)] + public string Tags + { + get => string.Join(",", tags); + set + { + tags.Clear(); + if (!string.IsNullOrWhiteSpace(value)) + { + string[] splitTags = value.Split(','); + foreach (var tag in splitTags) + { + tags.Add(tag.ToIdentifier()); + } + } + } + } + private readonly HashSet moduleFlags = new HashSet(); [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")] @@ -79,6 +105,12 @@ namespace Barotrauma public Identifier[] PreferredOutpostModuleTypes { get; protected set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.No)] + public Identifier Group { get; set; } + public XElement Element { get; protected set; } @@ -97,6 +129,11 @@ namespace Barotrauma this.NpcSetIdentifier = npcSetIdentifier; } + public IEnumerable GetTags() + { + return tags; + } + public IEnumerable GetModuleFlags() { return moduleFlags; @@ -148,7 +185,7 @@ namespace Barotrauma } } - public bool GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) + public bool GiveItems(Character character, Submarine submarine, WayPoint spawnPoint, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { if (ItemSets == null || !ItemSets.Any()) { return false; } var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element; @@ -159,7 +196,7 @@ namespace Barotrauma int amount = itemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); + InitializeItem(character, itemElement, submarine, this, spawnPoint, createNetworkEvents: createNetworkEvents); } } } @@ -177,17 +214,26 @@ namespace Barotrauma CharacterInfo characterInfo; if (characterElement == null) { - characterInfo= new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier); - } + characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: GetJobPrefab(randSync), npcIdentifier: Identifier, randSync: randSync);} else { characterInfo = new CharacterInfo(characterElement, Identifier); } + if (characterInfo.Job != null && !MathUtils.NearlyEqual(SkillMultiplier, 1.0f)) + { + foreach (var skill in characterInfo.Job.GetSkills()) + { + float newSkill = skill.Level * SkillMultiplier; + skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false); + } + characterInfo.Salary = characterInfo.CalculateSalary(); + } characterInfo.HumanPrefabIds = (NpcSetIdentifier, Identifier); + characterInfo.GiveExperience(ExperiencePoints); return characterInfo; } - public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) + public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -231,7 +277,7 @@ namespace Barotrauma IdCard idCardComponent = item.GetComponent(); if (idCardComponent != null) { - idCardComponent.Initialize(null, character); + idCardComponent.Initialize(spawnPoint, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; @@ -254,7 +300,7 @@ namespace Barotrauma int amount = childItemElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { - InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); + InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index 41a894960..e8e08f791 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -21,7 +21,7 @@ namespace Barotrauma level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } - private Identifier iconJobId; + private readonly Identifier iconJobId; public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab) ? jobPrefab.Icon diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 79d2eadc6..a58651d84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -666,13 +666,13 @@ namespace Barotrauma switch (body.BodyShape) { case PhysicsBody.Shape.Circle: - attack.DamageRange = body.radius; + attack.DamageRange = body.Radius; break; case PhysicsBody.Shape.Capsule: - attack.DamageRange = body.height / 2 + body.radius; + attack.DamageRange = body.Height / 2 + body.Radius; break; case PhysicsBody.Shape.Rectangle: - attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length(); + attack.DamageRange = new Vector2(body.Width / 2.0f, body.Height / 2.0f).Length(); break; } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); @@ -786,11 +786,12 @@ namespace Barotrauma } if (!foundMatchingModifier && random > affliction.Probability) { continue; } float finalDamageModifier = damageMultiplier; - if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp") + if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { finalDamageModifier *= character.EmpVulnerability; } - if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")) + if (!character.Params.Health.PoisonImmunity && + (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { finalDamageModifier *= character.PoisonVulnerability; } @@ -1108,7 +1109,7 @@ namespace Barotrauma Vector2 forceWorld = attack.CalculateAttackPhase(attack.RootTransitionEasing); forceWorld.X *= character.AnimController.Dir; character.AnimController.MainLimb.body.ApplyLinearImpulse(character.Mass * forceWorld, character.SimPosition, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - if (!attack.IsRunning) + if (!attack.IsRunning && !attack.Ranged) { // Set the main collider where the body lands after the attack if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) @@ -1303,7 +1304,8 @@ namespace Barotrauma } private float blinkTimer; - private float blinkPhase; + public float BlinkPhase; + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; @@ -1316,16 +1318,25 @@ namespace Barotrauma { if (blinkTimer > -TotalBlinkDurationOut) { - blinkPhase -= deltaTime; - if (blinkPhase > 0) + if (!FreezeBlinkState) + { + BlinkPhase -= deltaTime; + } + if (BlinkPhase > 0) { // in - float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn)); + float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, BlinkPhase / Params.BlinkDurationIn)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetOut, Params.BlinkTextureOffsetIn, t); +#endif + } } else { - if (Math.Abs(blinkPhase) < Params.BlinkHoldTime) + if (Math.Abs(BlinkPhase) < Params.BlinkHoldTime) { // hold body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true); @@ -1333,15 +1344,25 @@ namespace Barotrauma else { // out - float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + //float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut)); + float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, (-BlinkPhase - Params.BlinkHoldTime) / Params.BlinkDurationOut)); body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true); + if (Params.UseTextureOffsetForBlinking) + { +#if CLIENT + ActiveSprite.RelativeOrigin = Vector2.Lerp(Params.BlinkTextureOffsetIn, Params.BlinkTextureOffsetOut, t); +#endif + } } } } else { // out - blinkPhase = Params.BlinkDurationIn; + if (!FreezeBlinkState) + { + BlinkPhase = Params.BlinkDurationIn; + } body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index e844e53a0..e902ba353 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -177,6 +177,9 @@ namespace Barotrauma set => SetFootAngles(FootAnglesInRadians, value); } + [Serialize(false, IsPropertySaveable.Yes, description: "Should the animation be updated even if the character is not moving?"), Editable] + public bool UpdateAnimationWhenNotMoving { get; set; } + /// /// Key = limb id, value = angle in radians /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 04204dc96..ba88edee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -56,6 +56,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } + [Serialize(true, IsPropertySaveable.Yes), Editable] + public bool ShowHealthBar { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseBossHealthBar { get; private set; } @@ -110,6 +113,12 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool DrawLast { get; set; } + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable] + public float AITurretPriority { get; set; } + + [Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable] + public float AISlowTurretPriority { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -214,7 +223,7 @@ namespace Barotrauma return true; } - public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group; + public static bool CompareGroup(Identifier group1, Identifier group2) => group1 != Identifier.Empty && group2 != Identifier.Empty && group1 == group2; protected void CreateSubParams() { @@ -476,7 +485,7 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)] public float CrushDepth { get; set; } // Make editable? @@ -512,7 +521,20 @@ namespace Barotrauma // TODO: limbhealths, sprite? - public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) + { + //backwards compatibility + if (CrushDepth < 0) + { + //invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level) + float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000; + DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+ + "Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+ + "but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+ + $"Changing the crush depth from {CrushDepth} to {newCrushDepth}."); + CrushDepth = newCrushDepth; + } + } } public class InventoryParams : SubParam @@ -615,6 +637,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UsePathFindingToGetInside { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] public bool KeepDoorsClosed { get; private set; } @@ -823,10 +848,19 @@ namespace Barotrauma [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } - [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + [Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")] + public bool IgnoreTargetSize { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)] public float CircleRotationSpeed { get; private set; } - [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + [Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable] + public bool DynamicCircleRotationSpeed { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)] + public float CircleRandomRotationFactor { get; private set; } + + [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index d8f8992b0..be86bd432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -6,6 +6,7 @@ using System.Linq; using Barotrauma.IO; using System.Xml; using Barotrauma.Extensions; +using FarseerPhysics; #if CLIENT using Barotrauma.SpriteDeformations; #endif @@ -621,13 +622,13 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } [Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] @@ -706,6 +707,15 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyBlinkInWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool UseTextureOffsetForBlinking { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetIn { get; set; } + + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0f, MaxValueFloat = 1f)] + public Vector2 BlinkTextureOffsetOut { get; set; } + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionIn { get; private set; } @@ -1026,15 +1036,18 @@ namespace Barotrauma } } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Radius { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Height { get; set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 2048)] public float Width { get; set; } + [Serialize(BodyType.Dynamic, IsPropertySaveable.Yes), Editable] + public BodyType BodyType { get; set; } + public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) { Name = name; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 23512f751..a71667245 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -14,45 +14,51 @@ namespace Barotrauma.Abilities { string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; HashSet missionTypes = new HashSet(); + isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); + foreach (string missionTypeString in missionTypeStrings) { if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) { - DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); - return; + if (!isAffiliated) + { + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + } + continue; } missionTypes.Add(parsedMission); } missionType = missionTypes.ToImmutableHashSet(); - isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityMission { Mission: { } mission }) { - if (isAffiliated) + if (!isAffiliated) { return CheckMissionType(); } + + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } + + foreach (var (factionIdentifier, amount) in mission.ReputationRewards) { - if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; } - - foreach (var (factionIdentifier, amount) in mission.ReputationRewards) + if (amount <= 0) { continue; } + if (GetMatchingFaction(factionIdentifier) is { } faction && + Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) { - if (amount <= 0) { continue; } - - Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier); - - if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) - { - return true; - } + return CheckMissionType(); } - - return false; } - return missionType.Contains(mission.Prefab.Type); + return false; + + Faction GetMatchingFaction(Identifier factionIdentifier) => + factionIdentifier == "location" + ? mission.OriginLocation?.Faction + : factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier); + + bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type); } LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index c4017a87f..237e15b5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -23,7 +23,6 @@ protected override bool MatchesConditionSpecific() { Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); - return character.Info.GetSavedStatValue(statType, identifier) >= min; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs index 55c382e2a..7a4ceeb07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -4,6 +4,7 @@ using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma.Abilities { @@ -29,11 +30,33 @@ namespace Barotrauma.Abilities if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + ImmutableHashSet characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + HashSet> talentsTrees = new HashSet>(); foreach (TalentSubTree subTree in talentTree.TalentSubTrees) { if (subTree.Type != TalentTreeType.Specialization) { continue; } - talentsTrees.Add(subTree.AllTalentIdentifiers); + + HashSet identifiers = new HashSet(); + foreach (TalentOption option in subTree.TalentOptionStages) + { + foreach (Identifier identifier in option.TalentIdentifiers) + { + if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; } + + identifiers.Add(identifier); + } + + foreach (var (_, value) in option.ShowCaseTalents) + { + var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet(); + if (ids.Count is 0) { continue; } + + identifiers.Add(value.GetRandomUnsynced()); + } + } + + talentsTrees.Add(identifiers.ToImmutableHashSet()); } ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); @@ -44,6 +67,16 @@ namespace Barotrauma.Abilities Character.GiveTalent(identifier); } + + static bool IsShowCaseTalent(Identifier identifier, TalentOption option) + { + foreach (var (_, value) in option.ShowCaseTalents) + { + if (value.Contains(identifier)) { return true; } + } + + return false; + } } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index dc93405b4..d14c9df8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -131,6 +131,8 @@ namespace Barotrauma if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + if (IsTalentLocked(talentIdentifier)) { return false; } + foreach (var subTree in talentTree!.TalentSubTrees) { if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; } @@ -152,6 +154,18 @@ namespace Barotrauma return false; } + public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet characterList = null) + { + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + foreach (Character c in characterList) + { + if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; } + } + + return false; + } + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { List viableTalents = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs new file mode 100644 index 000000000..4c1ff0527 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SlideshowsFile.cs @@ -0,0 +1,15 @@ +namespace Barotrauma +{ + sealed class SlideshowsFile : GenericPrefabFile + { + protected override PrefabCollection Prefabs => SlideshowPrefab.Prefabs; + + public SlideshowsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "Slideshow"; + + protected override bool MatchesPlural(Identifier identifier) => identifier == "Slideshows"; + + protected override SlideshowPrefab CreatePrefab(ContentXElement element) => new SlideshowPrefab(this, element); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 213a0ed71..92e0b29ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -890,7 +890,15 @@ namespace Barotrauma ThrowError("Please specify an identifier and a value."); return; } - SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + if (float.TryParse(args[1], out float floatVal)) + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set); + } + else + { + SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set); + } + }, isCheat: true)); commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) => @@ -1092,11 +1100,6 @@ namespace Barotrauma commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null) { return; } - if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null) - { - NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red); - return; - } if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) { @@ -1261,6 +1264,22 @@ namespace Barotrauma } #endif + commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) => + { + if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + NewMessage("Reputation:"); + foreach (var faction in campaign.Factions) + { + NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}"); + } + } + else + { + ThrowError("Could not show reputation (no active campaign)."); + } + }, null)); + commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) => { if (GameMain.GameSession?.GameMode is CampaignMode campaign) @@ -1268,7 +1287,7 @@ namespace Barotrauma if (args.Length == 0) { return; } if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { - campaign.Map.CurrentLocation.Reputation.SetReputation(reputation); + campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation); } else { @@ -1425,7 +1444,7 @@ namespace Barotrauma commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) => { Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); - killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f); + killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); }, () => { @@ -1870,6 +1889,8 @@ namespace Barotrauma commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true)); commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true)); commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true)); + commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true)); InitProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index b9695d7dd..0b8c80af8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -146,6 +146,8 @@ namespace Barotrauma StoreSellMultiplier, StoreBuyMultiplierAffiliated, StoreBuyMultiplier, + ShipyardBuyMultiplierAffiliated, + ShipyardBuyMultiplier, MaxAttachableCount, ExplosionRadiusMultiplier, ExplosionDamageMultiplier, @@ -154,7 +156,8 @@ namespace Barotrauma HoldBreathMultiplier, Apprenticeship, Affiliation, - CPRBoost + CPRBoost, + LockedTalents } internal enum ItemTalentStats diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 2275bcd06..dcd1deeea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -110,7 +110,7 @@ namespace Barotrauma state = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return; + if (!Submarine.MainSub.AtEitherExit) { return; } Finish(); state = 2; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index e84bd7676..f808a9ce7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -33,7 +33,7 @@ namespace Barotrauma public int ItemContainerIndex { get; set; } private readonly IReadOnlyList conditionals; - + private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 1c5c8f0b5..a01b1abd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -200,7 +200,7 @@ namespace Barotrauma } } - private int[] GetEndingOptions() + public int[] GetEndingOptions() { List endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList(); if (!ContinueConversation) { endings.Add(-1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 2c298853d..957508205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,12 +1,13 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { - class MissionAction : EventAction + partial class MissionAction : EventAction { [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionIdentifier { get; set; } @@ -14,8 +15,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier MissionTag { get; set; } - [Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] - public string LocationType { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier RequiredFaction { get; set; } + + public ImmutableArray LocationTypes { get; } [Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] public int MinLocationDistance { get; set; } @@ -38,6 +41,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } + LocationTypes = element.GetAttributeIdentifierArray("locationtype", Array.Empty()).ToImmutableArray(); } public override bool IsFinished(ref string goTo) @@ -56,14 +60,14 @@ namespace Barotrauma if (GameMain.GameSession.GameMode is CampaignMode campaign) { Mission unlockedMission = null; - var unlockLocation = FindUnlockLocation(); + var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes); if (unlockLocation == null && CreateLocationIfNotFound) { //find an empty location at least 3 steps away, further on the map - var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet()); + var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable()); if (emptyLocation != null) { - emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]); + emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; } } @@ -72,7 +76,7 @@ namespace Barotrauma { if (!MissionIdentifier.IsEmpty) { - unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } else if (!MissionTag.IsEmpty) { @@ -84,7 +88,9 @@ namespace Barotrauma } if (unlockedMission != null) { - if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null) + unlockedMission.OriginLocation = campaign.Map.CurrentLocation; + campaign.Map.Discover(unlockLocation, checkTalents: false); + if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null) { DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\"."); } @@ -99,66 +105,86 @@ namespace Barotrauma IconColor = unlockedMission.Prefab.IconColor }; #else + missionsUnlockedThisRound.Add(unlockedMission); NotifyMissionUnlock(unlockedMission); - #endif +#endif } } else { - DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); + DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); } } isFinished = true; } - private Location FindUnlockLocation() + private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable locationTypes) { var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1) + if (LocationTypes.Length == 0 && minDistance <= 1) { return campaign.Map.CurrentLocation; } - return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet()); + var currentLocation = campaign.Map.CurrentLocation; + int distance = 0; + HashSet checkedLocations = new HashSet(); + HashSet pendingLocations = new HashSet() { currentLocation }; + do + { + List currentLocations = pendingLocations.ToList(); + pendingLocations.Clear(); + foreach (var location in currentLocations) + { + checkedLocations.Add(location); + if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes)) + { + return location; + } + else + { + foreach (LocationConnection connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (checkedLocations.Contains(otherLocation)) { continue; } + pendingLocations.Add(otherLocation); + } + } + } + distance++; + } while (pendingLocations.Any()); + + return null; } - private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) + private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable locationTypes) { - var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance && - (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) + if (!RequiredFaction.IsEmpty) { - return currLocation; + if (location.Faction?.Prefab.Identifier != RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != RequiredFaction) + { + return false; + } } - checkedLocations.Add(currLocation); - foreach (LocationConnection connection in currLocation.Connections) + if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier()))) { - var otherLocation = connection.OtherLocation(currLocation); - if (checkedLocations.Contains(otherLocation)) { continue; } - var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations); - if (unlockLocation != null) { return unlockLocation; } + return false; } - return null; + if (distance < minDistance) + { + return false; + } + if (unlockFurtherOnMap && location.MapPosition.X < currLocation.MapPosition.X) + { + return false; + } + return true; } public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } - -#if SERVER - private void NotifyMissionUnlock(Mission mission) - { - foreach (Client client in GameMain.Server.ConnectedClients) - { - IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); - outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); - outmsg.WriteIdentifier(mission.Prefab.Identifier); - outmsg.WriteString(mission.Name.Value); - GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); - } - } -#endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs new file mode 100644 index 000000000..d6deb9b1c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionStateAction.cs @@ -0,0 +1,66 @@ +namespace Barotrauma +{ + class MissionStateAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + public enum OperationType + { + Set, + Add + } + + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] + public OperationType Operation { get; set; } + + [Serialize(0, IsPropertySaveable.Yes)] + public int State { get; set; } + + private bool isFinished; + + public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + State = element.GetAttributeInt("value", State); + if (MissionIdentifier.IsEmpty) + { + DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured."); + } + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + foreach (Mission mission in GameMain.GameSession.Missions) + { + if (mission.Prefab.Identifier != MissionIdentifier) { continue; } + switch (Operation) + { + case OperationType.Set: + mission.State = State; + break; + case OperationType.Add: + mission.State += 1; + break; + } + } + + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionStateAction)} -> ({(Operation == OperationType.Set ? State : '+' + State)})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs new file mode 100644 index 000000000..013b48771 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ModifyLocationAction.cs @@ -0,0 +1,91 @@ +namespace Barotrauma +{ + class ModifyLocationAction : EventAction + { + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Faction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SecondaryFaction { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public string Name { get; set; } + + private bool isFinished; + + public ModifyLocationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + if (GameMain.GameSession.GameMode is CampaignMode campaign) + { + var location = campaign.Map.CurrentLocation; + if (location != null) + { + if (!Faction.IsEmpty) + { + var faction = campaign.Factions.Find(f => f.Prefab.Identifier == Faction); + if (faction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{Faction}\"."); + } + else + { + location.Faction = faction; + } + } + if (!SecondaryFaction.IsEmpty) + { + var secondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == SecondaryFaction); + if (secondaryFaction == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a faction with the identifier \"{SecondaryFaction}\"."); + } + else + { + location.SecondaryFaction = secondaryFaction; + } + } + if (!Type.IsEmpty) + { + var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == Type); + if (locationType == null) + { + DebugConsole.ThrowError($"Error in ModifyLocationAction ({ParentEvent.Prefab.Identifier}): could not find a location type with the identifier \"{Type}\"."); + } + else if (!location.LocationTypeChangesBlocked) + { + location.ChangeType(campaign, locationType); + } + } + if (!string.IsNullOrEmpty(Name)) + { + location.ForceName(TextManager.Get(Name).Fallback(Name).Value); + } + } + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ModifyLocationAction)}"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index ee26b9283..7905486c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,8 +1,6 @@ -using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -12,7 +10,7 @@ namespace Barotrauma public Identifier NPCTag { get; set; } [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] - public CharacterTeamType TeamTag { get; set; } + public CharacterTeamType TeamID { get; set; } [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } @@ -24,10 +22,13 @@ namespace Barotrauma public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); - if (!enums.Contains(TeamTag)) + if (!enums.Contains(TeamID)) { - DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamID}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); } } @@ -41,27 +42,34 @@ namespace Barotrauma foreach (var npc in affectedNpcs) { // characters will still remain on friendlyNPC team for rest of the tick - npc.SetOriginalTeam(TeamTag); - - if (AddToCrew && (TeamTag == CharacterTeamType.Team1 || TeamTag == CharacterTeamType.Team2)) + npc.SetOriginalTeam(TeamID); + foreach (Item item in npc.Inventory.AllItems) + { + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.TeamID = TeamID; + } + } + if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.AddCharacter(npc); ChangeItemTeam(Submarine.MainSub, true); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems)); } } else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2)) { npc.Info.StartItemsGiven = true; GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true); - var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamTag); + var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID); ChangeItemTeam(sub, false); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamTag, npc.Inventory.AllItems)); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems)); } } @@ -72,11 +80,10 @@ namespace Barotrauma item.AllowStealing = allowStealing; if (item.GetComponent() is { } wifiComponent) { - wifiComponent.TeamID = TeamTag; + wifiComponent.TeamID = TeamID; } if (item.GetComponent() is { } idCard) { - idCard.TeamID = TeamTag; idCard.SubmarineSpecificID = 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index cee49e531..e0d23114b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -39,13 +39,14 @@ namespace Barotrauma affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); foreach (var npc in affectedNpcs) { - if (!(npc.AIController is HumanAIController humanAiController)) { continue; } + if (npc.AIController is not HumanAIController humanAiController) { continue; } if (Follow) { var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + IsFollowOrderObjective = true }; humanAiController.ObjectiveManager.AddObjective(newObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index fa3b3d2f8..5d86e91d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -19,8 +19,6 @@ namespace Barotrauma private IEnumerable affectedNpcs; - private AIObjectiveGoTo gotoObjective; - public override void Update(float deltaTime) { if (isFinished) { return; } @@ -33,19 +31,17 @@ namespace Barotrauma if (Wait) { - gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) + var gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true) { - OverridePriority = 100.0f + OverridePriority = 100.0f, + SourceEventAction = this }; humanAiController.ObjectiveManager.AddObjective(gotoObjective); humanAiController.ObjectiveManager.WaitTimer = 0.0f; } else { - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + AbandonGoToObjectives(humanAiController); } } isFinished = true; @@ -62,17 +58,25 @@ namespace Barotrauma { foreach (var npc in affectedNpcs) { - if (npc.Removed || npc.AIController is not HumanAIController) { continue; } - if (gotoObjective != null) - { - gotoObjective.Abandon = true; - } + if (npc.Removed || npc.AIController is not HumanAIController aiController) { continue; } + AbandonGoToObjectives(aiController); } affectedNpcs = null; } isFinished = false; } + private void AbandonGoToObjectives(HumanAIController aiController) + { + foreach (var objective in aiController.ObjectiveManager.Objectives) + { + if (objective is AIObjectiveGoTo gotoObjective && gotoObjective.SourceEventAction?.ParentEvent == ParentEvent) + { + gotoObjective.Abandon = true; + } + } + } + public override string ToDebugString() { return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCWaitAction)} -> (NPCTag: {NPCTag.ColorizeObject()}, Wait: {Wait.ColorizeObject()})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index d39a5e666..348e856e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { @@ -11,17 +9,20 @@ namespace Barotrauma public Identifier TargetTag { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier ItemIdentifier { get; set; } + public string ItemIdentifiers { get; set; } [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) - { - if (ItemIdentifier.IsEmpty) + private readonly ImmutableHashSet itemIdentifierSplit; + + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + if (string.IsNullOrEmpty(ItemIdentifiers)) { - ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); + ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty)); } + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet(); } private bool isFinished = false; @@ -62,7 +63,7 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); + (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(it.Prefab.Identifier)), recursive: true); if (item == null) { break; } Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); @@ -70,7 +71,7 @@ namespace Barotrauma } else if (target is Item item) { - if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) + if (itemIdentifierSplit.Count == 0 || itemIdentifierSplit.Contains(item.Prefab.Identifier)) { Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index 14be7bdba..41c9391f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -46,43 +46,29 @@ namespace Barotrauma switch (TargetType) { case ReputationType.Faction: - { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); - if (faction != null) { - faction.Reputation.AddReputation(Increase); - } - else - { - DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); - } - - break; - } - case ReputationType.Location: - { - Location location = campaign.Map.CurrentLocation; - if (location != null) - { - location.Reputation.AddReputation(Increase); - IEnumerable locations = location.Connections.SelectMany(c => c.Locations).Distinct().Where(l => l != null && l != location); - foreach (Location connectedLocation in locations) + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); + if (faction != null) { - Debug.Assert(connectedLocation.Reputation != null, "connectedLocation.Reputation != null"); - if (connectedLocation.Reputation != null) - { - connectedLocation.Reputation.AddReputation(Increase / 4); - } + faction.Reputation.AddReputation(Increase); + } + else + { + DebugConsole.ThrowError($"Faction with the identifier \"{Identifier}\" was not found."); } - } - break; - } + break; + } + case ReputationType.Location: + { + campaign.Map.CurrentLocation?.Reputation?.AddReputation(Increase); + break; + } default: - { - DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); - break; - } + { + DebugConsole.ThrowError("ReputationAction requires a \"TargetType\" but none were specified."); + break; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index ea2851339..84d8eaa1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -50,7 +50,7 @@ namespace Barotrauma public Identifier SpawnPointTag { get; set; } [Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)] - public CharacterTeamType Team { get; protected set; } + public CharacterTeamType TeamID { get; protected set; } [Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")] public bool RequireSpawnPointTag { get; set; } @@ -92,6 +92,14 @@ namespace Barotrauma public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null; + //backwards compatibility + TeamID = element.GetAttributeEnum("teamtag", element.GetAttributeEnum("team", TeamID)); + if (element.GetAttribute("submarinetype") != null) + { + DebugConsole.ThrowError( + $"Error in even \"{(parentEvent.Prefab?.Identifier.ToString() ?? "unknown")}\". " + + $"The attribute \"submarinetype\" is not valid in {nameof(SpawnAction)}. Did you mean {nameof(SpawnLocation)}?"); + } } public override bool IsFinished(ref string goTo) @@ -118,7 +126,28 @@ namespace Barotrauma if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty) { - HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); + HumanPrefab humanPrefab = null; + if (Level.Loaded?.StartLocation is Location startLocation) + { + humanPrefab = + TryFindHumanPrefab(startLocation.Faction) ?? + TryFindHumanPrefab(startLocation.SecondaryFaction); + } + HumanPrefab TryFindHumanPrefab(Faction faction) + { + if (faction == null) { return null; } + return + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), faction.Prefab.Identifier), + logError: false) ?? + //try to spawn a coalition NPC if a correct one can't be found + NPCSet.Get(NPCSetIdentifier, + NPCIdentifier.Replace("[faction]".ToIdentifier(), "coalition".ToIdentifier()), + logError: false); + } + + humanPrefab ??= NPCSet.Get(NPCSetIdentifier, NPCIdentifier, logError: true); + if (humanPrefab != null) { if (!AllowDuplicates && @@ -130,13 +159,13 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } newCharacter.HumanPrefab = humanPrefab; - newCharacter.TeamID = Team; + newCharacter.TeamID = TeamID; newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint); if (LootingIsStealing) { foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) @@ -151,6 +180,14 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(newCharacter, tag); + } + } }); } } @@ -165,7 +202,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawn: newCharacter => { if (!TargetTag.IsEmpty && newCharacter != null) { @@ -211,7 +248,7 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); if (spawnPos != null) { - Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Offset), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned); } } else @@ -239,10 +276,10 @@ namespace Barotrauma spawned = true; } - public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount) + public static Vector2 OffsetSpawnPos(Vector2 pos, float offset) { - Hull hull = Hull.FindHull(pos); - pos += Rand.Vector(offsetAmount); + Hull hull = Hull.FindHull(pos); + pos += Rand.Vector(offset); if (hull != null) { float margin = 50.0f; @@ -289,30 +326,24 @@ namespace Barotrauma public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; - List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); - - potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); - + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable); if (moduleFlags != null && moduleFlags.Any()) { - List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList(); + var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains)); if (spawnPoints.Any()) { - potentialSpawnPoints = spawnPoints; + potentialSpawnPoints = spawnPoints.ToList(); } } - if (spawnpointTags != null && spawnpointTags.Any()) { - var spawnPoints = potentialSpawnPoints - .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed)); - + var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable)); if (requireTaggedSpawnPoint || spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); } } - if (potentialSpawnPoints.None()) { if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 4dc8d6adc..f07c316cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma { @@ -7,7 +6,7 @@ namespace Barotrauma { private readonly List effects = new List(); - private int actionIndex; + private readonly int actionIndex; [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -46,25 +45,40 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var targets = ParentEvent.GetTargets(TargetTag); + var eventTargets = ParentEvent.GetTargets(TargetTag); foreach (StatusEffect effect in effects) { - foreach (var target in targets) + foreach (var target in eventTargets) { - if (target is Item targetItem) + if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); - } - else - { - effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + List nearbyTargets = new List(); + effect.AddNearbyTargets(target.WorldPosition, nearbyTargets); + foreach (var nearbyTarget in nearbyTargets) + { + ApplyOnTarget(nearbyTarget as Entity, effect); + } + continue; } + ApplyOnTarget(target, effect); } } #if SERVER - ServerWrite(targets); + ServerWrite(eventTargets); #endif isFinished = true; + + void ApplyOnTarget(Entity target, StatusEffect effect) + { + if (target is Item targetItem) + { + effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + } + } } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 89eff4f39..ef66959e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -21,6 +21,9 @@ namespace Barotrauma [Serialize(true, IsPropertySaveable.Yes)] public bool IgnoreIncapacitatedCharacters { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool AllowHiddenItems { get; set; } + private bool isFinished = false; public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) @@ -119,12 +122,12 @@ namespace Barotrauma private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier); } private void TagItemsByTag(Identifier tag) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag)); } private void TagHullsByName(Identifier name) @@ -137,6 +140,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); } + private bool IsValidItem(Item it) + { + return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index 6380f8f0d..202a8734d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -1,6 +1,3 @@ -using System; -using System.Xml.Linq; - namespace Barotrauma { class WaitAction : EventAction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index dee7f7b0f..a6f01c406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -12,6 +12,7 @@ namespace Barotrauma public enum NetworkEventType { CONVERSATION, + CONVERSATION_SELECTED_OPTION, STATUSEFFECT, MISSION, UNLOCKPATH @@ -71,8 +72,8 @@ namespace Barotrauma private readonly List activeEvents = new List(); - private readonly HashSet finishedEvents = new HashSet(); - private readonly HashSet nonRepeatableEvents = new HashSet(); + private readonly HashSet finishedEvents = new HashSet(); + private readonly HashSet nonRepeatableEvents = new HashSet(); private readonly HashSet usedUniqueSets = new HashSet(); @@ -134,7 +135,9 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); activeEvents.Clear(); - +#if SERVER + MissionAction.ResetMissionsUnlockedThisRound(); +#endif pathFinder = new PathFinder(WayPoint.WayPointList, false); totalPathLength = 0.0f; if (level != null) @@ -151,7 +154,7 @@ namespace Barotrauma seed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); + seed ^= ToolBox.IdentifierToInt(previousEvent); } } rand = new MTRandom(seed); @@ -188,14 +191,7 @@ namespace Barotrauma //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => - e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData.Biome.Identifier); - - var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); + var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); @@ -216,7 +212,7 @@ namespace Barotrauma { foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { - nonRepeatableEvents.Add(ep); + nonRepeatableEvents.Add(ep.Identifier); } } foreach (EventSet childSet in eventSet.ChildSets) @@ -374,10 +370,10 @@ namespace Barotrauma { if (level?.LevelData == null) { return; } - level.LevelData.EventsExhausted = true; if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); + level.LevelData.EventsExhausted = true; + level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab.Identifier).Where(e => !level.LevelData.EventHistory.Contains(e))); if (level.LevelData.EventHistory.Count > MaxEventHistory) { level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); @@ -393,9 +389,9 @@ namespace Barotrauma private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { - if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; } float retVal = baseCommonness; - if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; } + if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; } return retVal; } @@ -436,9 +432,13 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) - => (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && - !level.LevelData.NonRepeatableEvents.Contains(e); + bool isPrefabSuitable(EventPrefab e) => + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) && + !level.LevelData.NonRepeatableEvents.Contains(e.Identifier) && + isFactionSuitable(e.Faction); + + bool isFactionSuitable(Identifier factionId) => + factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier; foreach (var subEventPrefab in eventSet.EventPrefabs) { @@ -447,9 +447,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\"."); } } - + var suitablePrefabSubsets = eventSet.EventPrefabs.Where( - e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); + e => isFactionSuitable(e.Faction) && e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); for (int i = 0; i < applyCount; i++) { @@ -601,6 +601,10 @@ namespace Barotrauma private bool IsValidForLocation(EventSet eventSet, Location location) { if (location is null) { return true; } + if (!eventSet.Faction.IsEmpty) + { + if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; } + } var locationType = location.GetLocationType(); bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents; if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; } @@ -728,53 +732,50 @@ namespace Barotrauma calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } - if (currentIntensity < eventThreshold) + bool recheck = false; + do { - bool recheck = false; - do + recheck = false; + //activate pending event sets that can be activated + for (int i = pendingEventSets.Count - 1; i >= 0; i--) { - recheck = false; - //activate pending event sets that can be activated - for (int i = pendingEventSets.Count - 1; i >= 0; i--) + var eventSet = pendingEventSets[i]; + if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } + if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; } + if (!CanStartEventSet(eventSet)) { continue; } + + pendingEventSets.RemoveAt(i); + + if (selectedEvents.ContainsKey(eventSet)) { - var eventSet = pendingEventSets[i]; - if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; } - - if (!CanStartEventSet(eventSet)) { continue; } - - pendingEventSets.RemoveAt(i); - - if (selectedEvents.ContainsKey(eventSet)) + //start events in this set + foreach (Event ev in selectedEvents[eventSet]) { - //start events in this set - foreach (Event ev in selectedEvents[eventSet]) + activeEvents.Add(ev); + eventThreshold = settings.DefaultEventThreshold; + if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) { - activeEvents.Add(ev); - eventThreshold = settings.DefaultEventThreshold; - if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) + eventCoolDown = settings.EventCooldown; + } + if (eventSet.ResetTime > 0) + { + ev.Finished += () => { - eventCoolDown = settings.EventCooldown; - } - if (eventSet.ResetTime > 0) - { - ev.Finished += () => - { - pendingEventSets.Add(eventSet); - CreateEvents(eventSet); - }; - } + pendingEventSets.Add(eventSet); + CreateEvents(eventSet); + }; } } - - //add child event sets to pending - foreach (EventSet childEventSet in eventSet.ChildSets) - { - pendingEventSets.Add(childEventSet); - recheck = true; - } } - } while (recheck); - } + + //add child event sets to pending + foreach (EventSet childEventSet in eventSet.ChildSets) + { + pendingEventSets.Add(childEventSet); + recheck = true; + } + } + } while (recheck); foreach (Event ev in activeEvents) { @@ -782,13 +783,13 @@ namespace Barotrauma { ev.Update(deltaTime); } - else if (!finishedEvents.Contains(ev)) + else if (ev.Prefab != null && !finishedEvents.Contains(ev.Prefab.Identifier)) { if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) { - if (!level.LevelData.EventHistory.Contains(ev.Prefab)) { level.LevelData.EventHistory.Add(ev.Prefab); } + if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); } } - finishedEvents.Add(ev); + finishedEvents.Add(ev.Prefab.Identifier); } } @@ -832,7 +833,7 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet || CharacterParams.CompareGroup(CharacterPrefab.HumanSpeciesName, character.Group)) { continue; } if (!(character.AIController is EnemyAIController enemyAI)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index db5fe38b8..9b7163f83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -1,6 +1,6 @@ using System; +using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { @@ -14,12 +14,12 @@ namespace Barotrauma public readonly bool TriggerEventCooldown; public readonly float Commonness; public readonly Identifier BiomeIdentifier; + public readonly Identifier Faction; public readonly float SpawnDistance; public readonly bool UnlockPathEvent; public readonly string UnlockPathTooltip; public readonly int UnlockPathReputation; - public readonly string UnlockPathFaction; public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) @@ -40,6 +40,7 @@ namespace Barotrauma } BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); + Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); @@ -47,7 +48,6 @@ namespace Barotrauma UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip"); UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0); - UnlockPathFaction = element.GetAttributeString("unlockpathfaction", ""); SpawnDistance = element.GetAttributeFloat("spawndistance", 0); } @@ -80,5 +80,17 @@ namespace Barotrauma { return $"EventPrefab ({Identifier})"; } + + public static EventPrefab GetUnlockPathEvent(Identifier biomeIdentifier, Faction faction) + { + var unlockPathEvents = Prefabs.OrderBy(p => p.Identifier).Where(e => e.UnlockPathEvent); + if (faction != null && unlockPathEvents.Any(e => e.Faction == faction.Prefab.Identifier)) + { + unlockPathEvents = unlockPathEvents.Where(e => e.Faction == faction.Prefab.Identifier); + } + return + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ?? + unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index a5f16c350..276edec6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -1,10 +1,9 @@ -using System; +using Barotrauma.Extensions; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -89,7 +88,9 @@ namespace Barotrauma public readonly LevelData.LevelType LevelType; public readonly ImmutableArray LocationTypeIdentifiers; - + + public readonly Identifier Faction; + public readonly bool ChooseRandom; private readonly int eventCount = 1; @@ -110,6 +111,8 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; + public readonly bool IgnoreIntensity; + public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; @@ -143,11 +146,12 @@ namespace Barotrauma public readonly struct SubEventPrefab { - public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) + public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability, Identifier factionId) { PrefabOrIdentifier = prefabOrIdentifiers; SelfCommonness = commonness; SelfProbability = probability; + Faction = factionId; } public readonly Either PrefabOrIdentifier; @@ -178,6 +182,8 @@ namespace Barotrauma public readonly float? SelfProbability; public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; + public readonly Identifier Faction; + public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { eventPrefabs = EventPrefabs; @@ -260,6 +266,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } + Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); + Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { @@ -282,6 +290,7 @@ namespace Barotrauma PerWreck = element.GetAttributeBool("perwreck", false); DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); + IgnoreIntensity = element.GetAttributeBool("ignoreintensity", parentSet?.IgnoreIntensity ?? false); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false)); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); @@ -332,15 +341,17 @@ namespace Barotrauma Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); + Identifier factionId = subElement.GetAttributeIdentifier(nameof(Faction), Identifier.Empty); eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, - probability >= 0f ? probability : (float?)null)); + probability >= 0f ? probability : (float?)null, + factionId)); } else { var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); - eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); + eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability, prefab.Faction)); } break; } @@ -365,8 +376,22 @@ namespace Barotrauma public float GetCommonness(Level level) { - Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; - return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; + if (level.GenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness)) + { + return generationParamsCommonness; + } + else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness)) + { + return startOutpostParamsCommonness; + } + else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null && + OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness)) + { + return endOutpostParamsCommonness; + } + return DefaultCommonness; } public int GetEventCount(Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index ced3da5c8..58d243d43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,5 +1,4 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -28,6 +27,8 @@ namespace Barotrauma private const float EndDelay = 5.0f; private float endTimer; + private bool allowOrderingRescuees; + public override bool AllowRespawn => false; public override bool AllowUndocking @@ -39,17 +40,17 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State == 0) { - return Enumerable.Empty(); + return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Targets.Select(t => t.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -83,6 +84,8 @@ namespace Barotrauma { characterConfig = prefab.ConfigElement.GetChildElement("Characters"); + allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true); + string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); @@ -144,10 +147,7 @@ namespace Barotrauma ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPoint == null) - { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) { @@ -186,7 +186,12 @@ namespace Barotrauma if (element.Attribute("identifier") != null && element.Attribute("from") != null) { - HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found"); + continue; + } for (int i = 0; i < count; i++) { LoadHuman(humanPrefab, element, submarine); @@ -198,7 +203,7 @@ namespace Barotrauma var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found"); continue; } for (int i = 0; i < count; i++) @@ -214,19 +219,25 @@ namespace Barotrauma { Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); + var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( - SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, + SpawnAction.SpawnLocationType.Outpost, spawnPointType, moduleFlags ?? humanPrefab.GetModuleFlags(), spawnPointTags ?? humanPrefab.GetSpawnPointTags(), element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); bool requiresRescue = element.GetAttributeBool("requirerescue", false); - - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos); + var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos); + if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) + { + outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag); + } + } if (spawnPos is WayPoint wp) { @@ -237,9 +248,19 @@ namespace Barotrauma { requireRescue.Add(spawnedCharacter); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + } #endif } + else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi) + { + var order = OrderPrefab.Prefabs["fightintruders"] + .CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false); + } if (element.GetAttributeBool("requirekill", false)) { @@ -252,10 +273,7 @@ namespace Barotrauma Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPos == null) - { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); - } + spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); if (element.GetAttributeBool("requirekill", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 2bfb34391..1a5a8cb3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -18,17 +18,19 @@ namespace Barotrauma private Ruin TargetRuin { get; set; } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State == 0) { - return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition); + return allTargets + .Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))) + .Select(t => (Prefab.SonarLabel, t.WorldPosition)); } else { - return Enumerable.Empty(); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } } @@ -164,7 +166,7 @@ namespace Barotrauma { bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ? campaign.GetAvailableTransition() != CampaignMode.TransitionType.None : - Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit); + Submarine.MainSub is { } sub && sub.AtEitherExit; return State > 0 && exitingLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 634652ffa..2350443aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -69,15 +69,7 @@ namespace Barotrauma } } - public override LocalizedString SonarLabel - { - get - { - return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel; - } - } - - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { @@ -85,7 +77,12 @@ namespace Barotrauma { yield break; } - yield return level.BeaconStation.WorldPosition; + else + { + yield return ( + Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, + level.BeaconStation.WorldPosition); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..daa064131 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -0,0 +1,295 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + partial class EndMission : Mission + { + enum MissionPhase + { + Initial, + NoItemsDestroyed, + SomeItemsDestroyed, + AllItemsDestroyed, + BossKilled + } + + private readonly CharacterPrefab bossPrefab; + private readonly CharacterPrefab minionPrefab; + + private readonly Identifier spawnPointTag; + private readonly Identifier destructibleItemTag; + + private readonly string endCinematicSound; + + private ImmutableArray minions; + private readonly int minionCount; + private readonly float minionScatter; + + private Character boss; + + private readonly ItemPrefab projectilePrefab; + + private float projectileTimer = 30.0f; + + private readonly float startCinematicDistance = 30.0f; + + private float endCinematicTimer; + + private readonly List destructibleItems = new List(); + + protected readonly float wakeUpCinematicDelay = 5.0f; + protected readonly float bossWakeUpDelay = 7.0f; + protected readonly float cameraWaitDuration = 7.0f; + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); } + } + + public override int State + { + get { return base.State; } + set + { + + if (state != value) + { + base.State = value; + OnStateChangedProjSpecific(); + if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.Invoke(() => + { + if (boss != null && !boss.Removed) + { + boss.AnimController.ColliderIndex = 1; + } + }, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2); + } + } + } + } + + private MissionPhase Phase + { + get + { + //state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss + //state 1: start cinematic played + //state 2: first destructibleItems destroyed + //state 3: 2nd destructibleItems destroyed + //state 4: all destructibleItems destroyed + //state 5: boss killed + if (state == 0) { return MissionPhase.Initial; } + if (state == 1) { return MissionPhase.NoItemsDestroyed; } + if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; } + if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; } + return MissionPhase.BossKilled; + } + } + + public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty); + if (!speciesName.IsEmpty) + { + bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (bossPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + else + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set."); + } + + Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty); + if (!minionName.IsEmpty) + { + minionPrefab = CharacterPrefab.FindBySpeciesName(minionName); + if (minionPrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + + minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255); + minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000); + + Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty); + if (!projectileId.IsEmpty) + { + projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab; + if (projectilePrefab == null) + { + DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\"."); + } + } + + spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty); + destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty); + endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty); + startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0); + } + + protected override void StartMissionSpecific(Level level) + { + var spawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag)); + if (spawnPoint == null) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\"."); + return; + } + if (!IsClient) + { + boss = Character.Create(bossPrefab.Identifier, spawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); + var minionList = new List(); + float angle = 0; + float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1); + for (int i = 0; i < minionCount; i++) + { + minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(spawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false)); + angle += angleStep; + } + SwarmBehavior.CreateSwarm(minionList.Cast()); + minions = minionList.ToImmutableArray(); + } + if (destructibleItemTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set."); + return; + } + destructibleItems.Clear(); + destructibleItems.AddRange(Item.ItemList.FindAll(it => it.HasTag(destructibleItemTag))); + if (destructibleItems.None()) + { + DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\"."); + return; + } + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + UpdateProjSpecific(); + + if (state == 0) + { + if (startCinematicDistance <= 0.0f || + boss == null || Submarine.MainSub == null || + Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance) + { + State = 1; + } + return; + } + + if (!IsClient && State > 0) + { + State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1); + } + + if (Phase == MissionPhase.AllItemsDestroyed) + { + if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed) + { + projectileTimer -= deltaTime; + if (projectileTimer <= 0.0f) + { + float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); + float distanceFactor = Math.Min(dist / 10000.0f, 1.0f); + int projectileAmount = Rand.Range(3, 6); + //more concentrated shots the further the sub is + float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f); + for (int i = 0; i < projectileAmount; i++) + { + int index = i; + Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it => + { + var projectile = it.GetComponent(); + float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition); + if (projectileAmount > 1) + { + angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread; + } + it.body.SetTransform(it.SimPosition, angle); + it.UpdateTransform(); + //faster launch velocity the further the sub is + projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor)); + }); + } + + //the closer the sub is, more likely it is to shoot frequently + float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor); + if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability) + { + projectileTimer = Rand.Range(3.0f, 5.0f); + } + else + { + projectileTimer = Rand.Range(15f, 30f); + } + } + } + else + { + State = Math.Max(destructibleItems.Count + 2, State); + } + } + else if (Phase == MissionPhase.BossKilled) + { + const float EndCinematicDuration = 20.0f; + + endCinematicTimer += deltaTime; +#if CLIENT + Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f); + + + Screen.Selected.Cam.Rotation = + Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f) + + (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration); + if (Rand.Range(0.0f, 100.0f) < endCinematicTimer) + { + Level.Loaded.Renderer.Flash(); + } + Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5; + Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition; + Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration; +#endif + if (endCinematicTimer > 5 && !IsClient) + { + foreach (Character c in Character.CharacterList) + { + if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null) + { + c.SetAllDamage(200.0f, 0.0f, 0.0f); + } + } + } + + if (endCinematicTimer > EndCinematicDuration && !IsClient) + { + //endCinematicTimer = 0; + GameMain.GameSession.Campaign?.LoadNewLevel(); + } + } + + } + + partial void UpdateProjSpecific(); + + partial void OnStateChangedProjSpecific(); + + protected override bool DetermineCompleted() + { + return Phase == MissionPhase.BossKilled; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index fe7c3ff84..51dd38d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -10,11 +10,12 @@ namespace Barotrauma { partial class EscortMission : Mission { - private readonly XElement characterConfig; - private readonly XElement itemConfig; + private readonly ContentXElement characterConfig; + private readonly ContentXElement itemConfig; private readonly List characters = new List(); private readonly Dictionary> characterItems = new Dictionary>(); + private readonly Dictionary> characterStatusEffects = new Dictionary>(); private readonly int baseEscortedCharacters; private readonly float scalingEscortedCharacters; @@ -28,7 +29,8 @@ namespace Barotrauma private readonly List terroristCharacters = new List(); private bool terroristsShouldAct = false; private float terroristDistanceSquared; - private const string TerroristTeamChangeIdentifier = "terrorist"; + private const string TerroristTeamChangeIdentifier = "terrorist"; + private readonly string terroristAnnounceDialogTag = string.Empty; public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) @@ -39,6 +41,7 @@ namespace Barotrauma scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems"); + terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty); CalculateReward(); } @@ -96,14 +99,27 @@ namespace Barotrauma } List humanPrefabsToSpawn = new List(); - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement characterElement in characterConfig.Elements()) { int count = CalculateScalingEscortedCharacterCount(inMission: true); - var humanPrefab = GetHumanPrefabFromElement(element); + var humanPrefab = GetHumanPrefabFromElement(characterElement); for (int i = 0; i < count; i++) { humanPrefabsToSpawn.Add(humanPrefab); } + foreach (var element in characterElement.Elements()) + { + if (element.NameAsIdentifier() == "statuseffect") + { + var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value); + if (newEffect == null) { continue; } + if (!characterStatusEffects.ContainsKey(humanPrefab)) + { + characterStatusEffects[humanPrefab] = new List { newEffect }; + } + characterStatusEffects[humanPrefab].Add(newEffect); + } + } } //if any of the escortees have a job defined, try to use a spawnpoint designated for that job @@ -128,6 +144,13 @@ namespace Barotrauma { humanAI.InitMentalStateManager(); } + if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList)) + { + foreach (var statusEffect in statusEffectList) + { + statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter); + } + } } @@ -162,7 +185,7 @@ namespace Barotrauma } int i = 0; - foreach (XElement element in characterConfig.Elements()) + foreach (ContentXElement element in characterConfig.Elements()) { string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty); string colorIdentifier = element.GetAttributeString("color", string.Empty); @@ -231,7 +254,10 @@ namespace Barotrauma if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) { character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); - character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + if (!string.IsNullOrEmpty(terroristAnnounceDialogTag)) + { + character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + } XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs index f0fa3a328..a1924db58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { partial class GoToMission : Mission { @@ -11,7 +13,7 @@ { if (Level.Loaded?.Type == LevelData.LevelType.Outpost) { - State = 1; + State = Math.Max(1, State); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 0d136f64f..2afe74f6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -50,13 +50,13 @@ namespace Barotrauma /// private readonly float resourceHandoverAmount; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { return missionClusterPositions - .Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1])) - .Select(p => p.Item2); + .Where(p => spawnedResources.ContainsKey(p.Identifier) && AnyAreUncollected(spawnedResources[p.Identifier])) + .Select(p => (ModifyMessage(Prefab.SonarLabel, color: false), p.Position)); } } @@ -64,7 +64,6 @@ namespace Barotrauma public override LocalizedString FailureMessage => ModifyMessage(base.FailureMessage); public override LocalizedString Description => ModifyMessage(description); public override LocalizedString Name => ModifyMessage(base.Name, false); - public override LocalizedString SonarLabel => ModifyMessage(base.SonarLabel, false); public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { @@ -175,7 +174,7 @@ namespace Barotrauma State = 1; break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 0ccb6fd13..c0f125362 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -22,7 +22,7 @@ namespace Barotrauma public virtual int State { get { return state; } - protected set + set { if (state != value) { @@ -30,6 +30,11 @@ namespace Barotrauma TryTriggerEvents(state); #if SERVER GameMain.Server?.UpdateMissionState(this); +#elif CLIENT + if (Prefab.ShowProgressBar) + { + CharacterHUD.ShowMissionProgressBar(this); + } #endif ShowMessage(State); OnMissionStateChanged?.Invoke(this); @@ -37,6 +42,8 @@ namespace Barotrauma } } + public int TimesAttempted { get; set; } + protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; private readonly CheckDataAction completeCheckDataAction; @@ -44,6 +51,12 @@ namespace Barotrauma public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; + /// + /// The reward that was actually given from completing the mission, taking any talent bonuses into account + /// (some of which may not be possible to determine in advance) + /// + private int? finalReward; + public virtual LocalizedString Name => Prefab.Name; private readonly LocalizedString successMessage; @@ -113,15 +126,19 @@ namespace Barotrauma get { return null; } } - public virtual IEnumerable SonarPositions + public virtual IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { - get { return Enumerable.Empty(); } + get { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } } - public virtual LocalizedString SonarLabel => Prefab.SonarLabel; - public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; + /// + /// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission. + /// Defaults to Locations[0] + /// + public Location OriginLocation; + public readonly Location[] Locations; public int? Difficulty @@ -141,7 +158,7 @@ namespace Barotrauma } } - private List delayedTriggerEvents = new List(); + private readonly List delayedTriggerEvents = new List(); public Action OnMissionStateChanged; @@ -157,12 +174,13 @@ namespace Barotrauma Headers = prefab.Headers; var messages = prefab.Messages.ToArray(); + OriginLocation = locations[0]; Locations = locations; var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction)); if (endConditionElement != null) { - completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})"); + completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})"); } for (int n = 0; n < 2; n++) @@ -307,7 +325,7 @@ namespace Barotrauma private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - if (trigger.Delay > 0) + if (trigger.Delay > 0 || trigger.State == 0) { if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger)) { @@ -357,6 +375,8 @@ namespace Barotrauma GiveReward(); } + TimesAttempted++; + EndMissionSpecific(completed); } @@ -364,6 +384,27 @@ namespace Barotrauma protected virtual void EndMissionSpecific(bool completed) { } + /// + /// Get the final reward, taking talent bonuses into account if the mission has concluded and the talents modified the reward accordingly. + /// + public int GetFinalReward(Submarine sub) + { + return finalReward ?? GetReward(sub); + } + + /// + /// Calculates the final reward after talent bonuses have been applied. Note that this triggers talent effects of the type OnGainMissionMoney, + /// and should only be called once when the mission is completed! + /// + private void CalculateFinalReward(Submarine sub) + { + int reward = GetReward(sub); + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + finalReward = (int)(reward * missionMoneyGainMultiplier.Value); + } private void GiveReward() { @@ -407,39 +448,35 @@ namespace Barotrauma info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value)); } - // apply money gains afterwards to prevent them from affecting XP gains - var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); - crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - - int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); - + CalculateFinalReward(Submarine.MainSub); #if SERVER - totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); + finalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), finalReward.Value); #endif bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (isSingleplayerOrServer && totalReward > 0) + if (isSingleplayerOrServer) { - campaign.Bank.Give(totalReward); - } - - foreach (Character character in crewCharacters) - { - character.Info.MissionsCompletedSinceDeath++; - } - - foreach (KeyValuePair reputationReward in ReputationRewards) - { - if (reputationReward.Key == "location") + if (finalReward > 0) { - Locations[0].Reputation.AddReputation(reputationReward.Value); - Locations[1].Reputation.AddReputation(reputationReward.Value); + campaign.Bank.Give(finalReward.Value); } - else + + foreach (Character character in crewCharacters) { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); - if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } + character.Info.MissionsCompletedSinceDeath++; + } + + foreach (KeyValuePair reputationReward in ReputationRewards) + { + if (reputationReward.Key == "location") + { + OriginLocation.Reputation?.AddReputation(reputationReward.Value); + } + else + { + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); + float prevValue = faction.Reputation.Value; + faction?.Reputation.AddReputation(reputationReward.Value); + } } } @@ -484,18 +521,15 @@ namespace Barotrauma float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); - return reward switch - { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), - None _ => (0, rewardPercentage, sum), - _ => throw new ArgumentOutOfRangeException() - }; + int amount = reward.TryUnwrap(out var a) ? a : 0; + + return ((int)(amount * rewardWeight), rewardPercentage, sum); } protected void ChangeLocationType(LocationTypeChange change) { if (change == null) { throw new ArgumentException(); } - if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) + if (GameMain.GameSession.GameMode is CampaignMode campaign && !IsClient) { int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) @@ -509,13 +543,15 @@ namespace Barotrauma if (srcIndex == -1) { return; } var location = Locations[srcIndex]; + if (location.LocationTypeChangesBlocked) { return; } + if (change.RequiredDurationRange.X > 0) { location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); } else { - location.ChangeType(LocationType.Prefabs[change.ChangeToType]); + location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -529,7 +565,6 @@ namespace Barotrauma if (element.Attribute("name") != null) { DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); - return null; } @@ -538,7 +573,7 @@ namespace Barotrauma HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { - DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found"); + DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); return null; } @@ -557,8 +592,7 @@ namespace Barotrauma Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); - + humanPrefab.GiveItems(spawnedCharacter, submarine, positionToStayIn as WayPoint, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 92f08baec..1c2663dcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -25,7 +25,8 @@ namespace Barotrauma GoTo = 0x400, ScanAlienRuins = 0x800, ClearAlienRuins = 0x1000, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins + End = 0x2000, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End } partial class MissionPrefab : PrefabWithUintIdentifier @@ -45,14 +46,15 @@ namespace Barotrauma { MissionType.Pirate, typeof(PirateMission) }, { MissionType.GoTo, typeof(GoToMission) }, { MissionType.ScanAlienRuins, typeof(ScanMission) }, - { MissionType.ClearAlienRuins, typeof(AlienRuinMission) } + { MissionType.ClearAlienRuins, typeof(AlienRuinMission) }, + { MissionType.End, typeof(EndMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { { MissionType.Combat, typeof(CombatMission) } }; - public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo, MissionType.End }; private readonly ConstructorInfo constructor; @@ -62,11 +64,7 @@ namespace Barotrauma public readonly Identifier TextIdentifier; - private readonly string[] tags; - public IEnumerable Tags - { - get { return tags; } - } + public readonly ImmutableHashSet Tags; public readonly LocalizedString Name; public readonly LocalizedString Description; @@ -93,10 +91,24 @@ namespace Barotrauma public readonly bool AllowRetry; + public readonly bool ShowInMenus, ShowStartMessage; + public readonly bool IsSideObjective; + public readonly bool AllowOtherMissionsInLevel; + public readonly bool RequireWreck, RequireRuin; + /// + /// If enabled, locations this mission takes place in cannot change their type + /// + public readonly bool BlockLocationTypeChanges; + + public readonly bool ShowProgressBar; + public readonly bool ShowProgressInNumbers; + public readonly int MaxProgressState; + public readonly LocalizedString ProgressBarLabel; + /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// @@ -144,7 +156,7 @@ namespace Barotrauma TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); - tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + Tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); string nameTag = element.GetAttributeString("name", ""); Name = TextManager.Get($"MissionName.{TextIdentifier}"); @@ -167,16 +179,26 @@ namespace Barotrauma Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); + ShowInMenus = element.GetAttributeBool("showinmenus", true); + ShowStartMessage = element.GetAttributeBool("showstartmessage", true); IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); + BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false); Commonness = element.GetAttributeInt("commonness", 1); + AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true); if (element.GetAttribute("difficulty") != null) { int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } + ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false); + ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false); + MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1); + string progressBarLabel = element.GetAttributeString(nameof(ProgressBarLabel), ""); + ProgressBarLabel = TextManager.Get(progressBarLabel).Fallback(progressBarLabel); + string successMessageTag = element.GetAttributeString("successmessage", ""); SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); if (!string.IsNullOrEmpty(successMessageTag)) @@ -350,6 +372,7 @@ namespace Barotrauma { return AllowedLocationTypes.Any(lt => lt == "any") || + AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 9b3641502..a9ab792ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -16,17 +16,20 @@ namespace Barotrauma private readonly Level.PositionType spawnPosType; private Vector2? spawnPos = null; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - return Enumerable.Empty(); + yield break; } else { - return sonarPositions; + foreach (Vector2 sonarPos in sonarPositions) + { + yield return (Prefab.SonarLabel, sonarPos); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index bc71d49dc..278a4d0fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -31,17 +31,17 @@ namespace Barotrauma private Vector2 nestPosition; - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0) { - Enumerable.Empty(); + yield break; } else { - yield return nestPosition; + yield return (Prefab.SonarLabel, nestPosition); } } } @@ -274,7 +274,7 @@ namespace Barotrauma break; case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + if (!Submarine.MainSub.AtEitherExit) { return; } State = 2; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index ff33e7ec9..ecc29fcb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -36,23 +36,32 @@ namespace Barotrauma private readonly List patrolPositions = new List(); - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - var empty = Enumerable.Empty(); - if (outsideOfSonarRange) + if (!outsideOfSonarRange || state > 1) { - return State switch - { - 0 => patrolPositions, - 1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty, - _ => empty, - }; + yield break; + } - else + else if (state == 0) { - return empty; + foreach (Vector2 patrolPos in patrolPositions) + { + yield return (Prefab.SonarLabel, patrolPos); + } + } + else if (state == 1) + { + if (lastSighting.HasValue) + { + yield return (Prefab.SonarLabel, lastSighting.Value); + } + else + { + yield break; + } } } } @@ -85,6 +94,31 @@ namespace Barotrauma characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); + //make sure all referenced character types are defined + foreach (XElement characterElement in characterConfig.Elements()) + { + var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty); + var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId); + if (characterTypeElement == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\"."); + } + } + //make sure all defined character types can be found from human prefabs + foreach (XElement characterTypeElement in characterTypeConfig.Elements()) + { + foreach (XElement characterElement in characterTypeElement.Elements()) + { + Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\"."); + } + } + } + // for campaign missions, set level at construction LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData; if (levelData != null) @@ -100,6 +134,7 @@ namespace Barotrauma //level already set return; } + submarineInfo = null; levelData = level; missionDifficulty = level?.Difficulty ?? 0; @@ -117,8 +152,15 @@ namespace Barotrauma DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } - // maybe a little redundant - var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(x => x.Path == submarinePath); + + BaseSubFile contentFile = + GetSubFile(submarinePath) ?? + GetSubFile(submarinePath); + BaseSubFile GetSubFile(ContentPath path) where T : BaseSubFile + { + return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(f => f.Path == submarinePath); + } + if (contentFile == null) { DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); @@ -241,9 +283,10 @@ namespace Barotrauma // it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional // if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand); + var characterId = element.GetAttributeString("typeidentifier", string.Empty); for (int i = 0; i < amountCreated; i++) { - XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault(); + XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault(); if (characterType == null) { @@ -253,7 +296,10 @@ namespace Barotrauma XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); - Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null); + var humanPrefab = GetHumanPrefabFromElement(variantElement); + if (humanPrefab == null) { continue; } + + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null); if (!commanderAssigned) { bool isCommander = variantElement.GetAttributeBool("iscommander", false); @@ -305,8 +351,9 @@ namespace Barotrauma if (enemySub == null) { - DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined."); - // TODO: should we set the state to something here? + DebugConsole.ThrowError(submarineInfo == null ? + $"Error in PirateMission: enemy sub was not created (submarineInfo == null)." : + $"Error in PirateMission: enemy sub was not created."); return; } @@ -345,10 +392,11 @@ namespace Barotrauma protected override void UpdateMissionSpecific(float deltaTime) { - if (state >= 2) { return; } + if (state >= 2 || enemySub == null) { return; } float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange); outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange; + if (CheckWinState()) { State = 2; @@ -411,6 +459,7 @@ namespace Barotrauma characters.Clear(); characterItems.Clear(); failed = !completed; + submarineInfo = null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 0d1b41b98..273c38f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -5,40 +5,182 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { partial class SalvageMission : Mission { - private readonly ItemPrefab itemPrefab; - private Item item; - - private readonly Level.PositionType spawnPositionType; - - private readonly string containerTag; - - private readonly string existingItemTag; - - private readonly bool showMessageWhenPickedUp; - - /// - /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. - /// - private readonly List> statusEffects = new List>(); - - public override IEnumerable SonarPositions + private class Target { - get + public Item Item; + + /// + /// Note that the integer values matter here: the state of the target can't go back to a smaller value, + /// and a larger or equal value than the RequiredRetrievalState means the item counts as retrieved + /// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub) + /// + public enum RetrievalState + { + None = 0, + Interact = 1, + PickedUp = 2, + RetrievedToSub = 3 + } + + public readonly ItemPrefab ItemPrefab; + public readonly Level.PositionType SpawnPositionType; + public readonly string ContainerTag; + public readonly string ExistingItemTag; + + public readonly bool RemoveItem; + + public readonly LocalizedString SonarLabel; + + public readonly bool AllowContinueBeforeRetrieved; + + /// + /// Does the target need to be picked up or brought to the sub for mission to be considered successful. + /// If None, the target has no effect on the completion of the mission. + /// + public readonly RetrievalState RequiredRetrievalState; + + public readonly bool HideLabelAfterRetrieved; + + public bool Retrieved { - if (item == null) + get { - Enumerable.Empty(); + return RequiredRetrievalState switch + { + RetrievalState.None => true, + RetrievalState.Interact or RetrievalState.PickedUp => State >= RequiredRetrievalState, + RetrievalState.RetrievedToSub => State == RetrievalState.RetrievedToSub, + _ => throw new NotImplementedException(), + }; + } + } + + private RetrievalState state; + public RetrievalState State + { + get { return state; } + set + { + if (value == state) { return; } + state = value; +#if SERVER + GameMain.Server?.UpdateMissionState(mission); +#endif + } + } + + public bool Interacted; + + private readonly SalvageMission mission; + + /// + /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. + /// + public readonly List> StatusEffects = new List>(); + + public Target(ContentXElement element, SalvageMission mission) + { + this.mission = mission; + ContainerTag = element.GetAttributeString("containertag", ""); + RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", RetrievalState.RetrievedToSub); + AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", false); + HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", false); + + string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); + if (!string.IsNullOrEmpty(sonarLabelTag)) + { + SonarLabel = + TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") + .Fallback(TextManager.Get(sonarLabelTag)) + .Fallback(element.GetAttributeString("sonarlabel", "")); + } + ExistingItemTag = element.GetAttributeString("existingitemtag", ""); + + RemoveItem = element.GetAttributeBool("removeitem", true); + + if (element.GetAttribute("itemname") != null) + { + DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); + string itemName = element.GetAttributeString("itemname", ""); + ItemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission: couldn't find an item prefab with the name \"{itemName}\""); + } } else { - yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition; + Identifier itemIdentifier = element.GetAttributeIdentifier("itemidentifier", Identifier.Empty); + if (!itemIdentifier.IsEmpty) + { + ItemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; + } + if (ItemPrefab == null) + { + string itemTag = element.GetAttributeString("itemtag", ""); + ItemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; + } + if (ItemPrefab == null && ExistingItemTag.IsNullOrEmpty()) + { + DebugConsole.ThrowError($"Error in SalvageMission - couldn't find an item prefab with the identifier \"{itemIdentifier}\""); + } + } + + SpawnPositionType = element.GetAttributeEnum("spawntype", Level.PositionType.Cave | Level.PositionType.Ruin); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Add(new List { newEffect }); + break; + } + case "chooserandom": + StatusEffects.Add(new List()); + foreach (var effectElement in subElement.Elements()) + { + var newEffect = StatusEffect.Load(effectElement, parentDebugName: mission.Prefab.Name.Value); + if (newEffect == null) { continue; } + StatusEffects.Last().Add(newEffect); + } + break; + } + } + } + + public void Reset() + { + state = RetrievalState.None; + Item = null; + } + } + + private readonly List targets = new List(); + + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels + { + get + { + foreach (var target in targets) + { + if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; } + if (target.Item != null) + { + yield return ( + target.SonarLabel ?? Prefab.SonarLabel, + target.Item.GetRootInventoryOwner()?.WorldPosition ?? target.Item.WorldPosition); + } + if (!target.AllowContinueBeforeRetrieved && !target.Retrieved) { break; } } } } @@ -46,225 +188,251 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); - - if (prefab.ConfigElement.GetAttribute("itemname") != null) + foreach (ContentXElement subElement in prefab.ConfigElement.Elements()) { - DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); - string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) + if (subElement.NameAsIdentifier() == "target") { - DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName); + targets.Add(new Target(subElement, this)); } } - else + if (!targets.Any()) { - string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); - if (itemIdentifier != null) - { - itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; - } - if (itemPrefab == null) - { - string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", ""); - itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab; - } - if (itemPrefab == null) - { - DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier); - } - } - - existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", ""); - showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false); - - string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || - !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) - { - spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; - } - - foreach (var element in prefab.ConfigElement.Elements()) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "statuseffect": - { - var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Add(new List { newEffect }); - break; - } - case "chooserandom": - statusEffects.Add(new List()); - foreach (var subElement in element.Elements()) - { - var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value); - if (newEffect == null) { continue; } - statusEffects.Last().Add(newEffect); - } - break; - } + targets.Add(new Target(prefab.ConfigElement, this)); } } protected override void StartMissionSpecific(Level level) { #if SERVER - originalInventoryID = Entity.NullEntityID; + spawnInfo.Clear(); #endif - item = null; - if (!IsClient) + foreach (var target in targets) { - //ruin/cave/wreck items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? - 0.0f : Level.Loaded.Size.X * 0.3f; - Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); - - if (!string.IsNullOrEmpty(existingItemTag)) + bool usedExistingItem = false; + UInt16 originalInventoryID = 0; + byte originalItemContainerIndex = 0; + int originalSlotIndex = 0; + var executedEffectIndices = new List<(int listIndex, int effectIndex)>(); + + target.Reset(); + if (!IsClient) { - var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag)); - switch (spawnPositionType) + //ruin/cave/wreck items are allowed to spawn close to the sub + float minDistance = target.SpawnPositionType switch { - case Level.PositionType.Cave: - case Level.PositionType.MainPath: - case Level.PositionType.SidePath: - item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); - break; - case Level.PositionType.Ruin: - case Level.PositionType.Wreck: - foreach (Item it in suitableItems) - { - if (it.Submarine?.Info == null) { continue; } - if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } - if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } - Rectangle worldBorders = it.Submarine.Borders; - worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); - if (Submarine.RectContains(worldBorders, it.WorldPosition)) - { - item = it; -#if SERVER - usedExistingItem = true; -#endif - break; - } - } - break; - } - } + Level.PositionType.Ruin or + Level.PositionType.Cave or + Level.PositionType.Wreck or + Level.PositionType.Outpost => 0.0f, + _ => Level.Loaded.Size.X * 0.3f, + }; + Vector2 position = + target.SpawnPositionType == Level.PositionType.None ? + Vector2.Zero : + Level.Loaded.GetRandomItemPos(target.SpawnPositionType, 100.0f, minDistance, 30.0f); - if (item == null) - { - item = new Item(itemPrefab, position, null); - item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation); - item.body.FarseerBody.BodyType = BodyType.Kinematic; - } - - for (int i = 0; i < statusEffects.Count; i++) - { - List effectList = statusEffects[i]; - if (effectList.Count == 0) { continue; } - int effectIndex = Rand.Int(effectList.Count); - var selectedEffect = effectList[effectIndex]; - item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); -#if SERVER - executedEffectIndices.Add(new Pair(i, effectIndex)); -#endif - } - - //try to find a container and place the item inside it - if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) - { - List validContainers = new List(); - foreach (Item it in Item.ItemList) + if (!string.IsNullOrEmpty(target.ExistingItemTag)) { - if (!it.HasTag(containerTag)) { continue; } - if (!it.IsPlayerTeamInteractable) { continue; } - switch (spawnPositionType) + var suitableItems = Item.ItemList.Where(it => it.HasTag(target.ExistingItemTag)); + if (GameMain.GameSession?.Missions != null) + { + //don't choose an item that was already chosen as the target for another salvage mission + suitableItems = suitableItems.Where(it => + GameMain.GameSession.Missions.None(m => m != this && m is SalvageMission salvageMission && salvageMission.targets.Any(t => t.Item == it))); + } + switch (target.SpawnPositionType) { case Level.PositionType.Cave: case Level.PositionType.MainPath: - if (it.Submarine != null) { continue; } + case Level.PositionType.SidePath: + target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; case Level.PositionType.Ruin: - if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } - break; case Level.PositionType.Wreck: - if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + case Level.PositionType.Outpost: + foreach (Item it in suitableItems) + { + if (it.Submarine?.Info == null) { continue; } + if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } + if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; } + Rectangle worldBorders = it.Submarine.Borders; + worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, it.WorldPosition)) + { + target.Item = it; +#if SERVER + usedExistingItem = true; +#endif + break; + } + } + break; + default: + target.Item = suitableItems.FirstOrDefault(); +#if SERVER + usedExistingItem = target.Item != null; +#endif break; } - var itemContainer = it.GetComponent(); - if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); } } - if (validContainers.Any()) + + if (target.Item == null) { - var selectedContainer = validContainers.GetRandomUnsynced(); - if (selectedContainer.Combine(item, user: null)) + if (target.ItemPrefab == null && string.IsNullOrEmpty(target.ContainerTag)) { + DebugConsole.ThrowError($"Failed to find a target item for the mission \"{Prefab.Identifier}\". Item tag: {target.ExistingItemTag ?? "null"}"); + continue; + } + target.Item = new Item(target.ItemPrefab, position, null); + target.Item.body.SetTransformIgnoreContacts(target.Item.body.SimPosition, target.Item.body.Rotation); + target.Item.body.FarseerBody.BodyType = BodyType.Kinematic; + } + else if (target.RequiredRetrievalState == Target.RetrievalState.Interact) + { + target.Item.OnInteract += () => + { + target.Interacted = true; + }; + } + for (int i = 0; i < target.StatusEffects.Count; i++) + { + List effectList = target.StatusEffects[i]; + if (effectList.Count == 0) { continue; } + int effectIndex = Rand.Int(effectList.Count); + var selectedEffect = effectList[effectIndex]; + target.Item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: target.Item.Position); #if SERVER - originalInventoryID = selectedContainer.Item.ID; - originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); - originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1; + executedEffectIndices.Add((i, effectIndex)); #endif - } // Placement successful + } + + //try to find a container and place the item inside it + if (!string.IsNullOrEmpty(target.ContainerTag) && target.Item.ParentInventory == null) + { + List validContainers = new List(); + foreach (Item it in Item.ItemList) + { + if (!it.HasTag(target.ContainerTag)) { continue; } + if (!it.IsPlayerTeamInteractable) { continue; } + switch (target.SpawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + if (it.Submarine != null) { continue; } + break; + case Level.PositionType.Ruin: + if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } + break; + case Level.PositionType.Wreck: + if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + break; + } + var itemContainer = it.GetComponent(); + if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); } + } + if (validContainers.Any()) + { + var selectedContainer = validContainers.GetRandomUnsynced(); + if (selectedContainer.Combine(target.Item, user: null)) + { +#if SERVER + originalInventoryID = selectedContainer.Item.ID; + originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); + originalSlotIndex = target.Item.ParentInventory?.FindIndex(target.Item) ?? -1; +#endif + } // Placement successful + } } } +#if SERVER + spawnInfo.Add( + target, + new SpawnInfo(usedExistingItem, originalInventoryID, originalItemContainerIndex, originalSlotIndex, executedEffectIndices)); +#endif } } protected override void UpdateMissionSpecific(float deltaTime) { - if (item == null) + //make body dynamic when picked up + foreach (var target in targets) { -#if DEBUG - DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); -#endif - return; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root == null) { continue; } + if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; } } - if (IsClient) + if (IsClient) { return; } + + for (int i = 0; i < targets.Count; i++) { - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - return; - } - switch (State) - { - case 0: - if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - if (showMessageWhenPickedUp) - { - if (!(item.GetRootInventoryOwner() is Character)) { return; } - } - else - { - Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; - if (parentSub == null || parentSub.Info.Type != SubmarineType.Player) + var target = targets[i]; + if (i > 0 && !targets[i - 1].AllowContinueBeforeRetrieved && !targets[i - 1].Retrieved) { break; } + if (target.Item == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); +#endif + return; + } + switch (target.State) + { + case Target.RetrievalState.None: + if (target.Interacted) { - return; + TrySetRetrievalState(Target.RetrievalState.Interact); } - } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) + { + TrySetRetrievalState(Target.RetrievalState.PickedUp); + } + break; + case Target.RetrievalState.PickedUp: + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; + if (parentSub != null && parentSub.Info.Type == SubmarineType.Player) + { + TrySetRetrievalState(Target.RetrievalState.RetrievedToSub); + } + break; + } + + void TrySetRetrievalState(Target.RetrievalState retrievalState) + { + if (retrievalState < target.State) { return; } + bool wasRetrieved = false; + target.State = retrievalState; + //increment the mission state if the target became retrieved + if (!wasRetrieved && target.Retrieved) { State = i + 1; } + } + } + if (targets.All(t => t.Retrieved)) + { + State = targets.Count + 1; } } protected override bool DetermineCompleted() { - var root = item?.GetRootContainer() ?? item; - return root?.CurrentHull?.Submarine != null && (root.CurrentHull.Submarine.AtEndExit || root.CurrentHull.Submarine.AtStartExit) && !item.Removed; + return targets.All(t => t.State >= t.RequiredRetrievalState); } protected override void EndMissionSpecific(bool completed) { - item?.Remove(); - item = null; - failed = !completed && state > 0; + //consider failed (can't attempt again) if we picked up any of the items but failed to bring them out of the level + failed = !completed && targets.Any(t => t.State >= Target.RetrievalState.PickedUp); + foreach (var target in targets) + { + if (target.RemoveItem) + { + target.Item?.Remove(); + target.Reset(); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index a59c69f8d..b8a2f0936 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -32,25 +32,20 @@ namespace Barotrauma } } - public override IEnumerable SonarPositions + public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { - if (State > 0) + if (State > 0 || scanTargets.None()) { - return Enumerable.Empty(); - } - else if (scanTargets.Any()) - { - return scanTargets - .Where(kvp => !kvp.Value) - .Select(kvp => kvp.Key.WorldPosition); + return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } else { - return Enumerable.Empty(); - } - + return scanTargets + .Where(kvp => !kvp.Value) + .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index cbb841a31..703aa19ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -116,7 +116,7 @@ namespace Barotrauma { foreach (Entity entity in Entity.GetEntities()) { - if (targetPredicates[tag].Any(p => p(entity))) + if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity)) { targetsToReturn.Add(entity); } @@ -131,7 +131,7 @@ namespace Barotrauma { foreach (Character npc in outpostNPCs) { - if (npc.Removed) { continue; } + if (npc.Removed || targetsToReturn.Contains(npc)) { continue; } targetsToReturn.Add(npc); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 7aa700475..272d38833 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -82,7 +82,13 @@ namespace Barotrauma.Extensions return count == 0 ? default : source.ElementAt(Rand.Range(0, count, Rand.RandSync.Unsynced)); } } - + + public static T GetRandom(this IEnumerable source, Random rand) + where T : PrefabWithUintIdentifier + { + return source.OrderBy(p => p.UintIdentifier).ToArray().GetRandom(rand); + } + public static T GetRandom(this IEnumerable source, Rand.RandSync randSync) where T : PrefabWithUintIdentifier { @@ -305,11 +311,14 @@ namespace Barotrauma.Extensions => source .Where(nullable => nullable.HasValue) .Select(nullable => nullable.Value); - + public static IEnumerable NotNone(this IEnumerable> source) - => source - .OfType>() - .Select(some => some.Value); + { + foreach (var o in source) + { + if (o.TryUnwrap(out var v)) { yield return v; } + } + } public static IEnumerable Successes( this IEnumerable> source) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index edcf1532a..a2ab60f25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using Barotrauma.Networking; +using System.Collections; #if SERVER using Barotrauma.Networking; #endif @@ -471,6 +472,21 @@ namespace Barotrauma return true; } + public static IEnumerable FindCargoRooms(IEnumerable subs) => subs.SelectMany(s => FindCargoRooms(s)); + + public static IEnumerable FindCargoRooms(Submarine sub) => WayPoint.WayPointList + .Where(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Cargo) + .Select(wp => wp.CurrentHull) + .Distinct(); + + public static IEnumerable FilterCargoCrates(IEnumerable items, Func conditional = null) + => items.Where(it => it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it))); + + public static IEnumerable FindReusableCargoContainers(IEnumerable subs, IEnumerable cargoRooms = null) => + FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull))) + .Select(it => it.GetComponent()) + .Where(c => c != null); + public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List availableContainers) { ItemContainer itemContainer = null; @@ -553,8 +569,8 @@ namespace Barotrauma } #endif } - - List availableContainers = new List(); + var connectedSubs = sub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + List availableContainers = FindReusableCargoContainers(connectedSubs, FindCargoRooms(connectedSubs)).ToList(); foreach (PurchasedItem pi in itemsToSpawn) { Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 06ab5566e..03839f12f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -248,11 +248,11 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) + if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost()) { - spawnWaypoints = WayPoint.WayPointList.FindAll(wp => + spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && - wp.Submarine == Level.Loaded.StartOutpost && + wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) @@ -262,9 +262,8 @@ namespace Barotrauma while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count) { spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]); - } + } } - if (spawnWaypoints == null || !spawnWaypoints.Any()) { spawnWaypoints = mainSubWaypoints; @@ -290,6 +289,16 @@ namespace Barotrauma else if (!character.Info.StartItemsGiven) { character.GiveJobItems(mainSubWaypoints[i]); + foreach (Item item in character.Inventory.AllItems) + { + //if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in + //we don't want that in this case, the crew's cards shouldn't be submarine-specific + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.SubmarineSpecificID = 0; + } + } } if (character.Info.HealthData != null) { @@ -298,6 +307,7 @@ namespace Barotrauma character.LoadTalents(); + character.GiveIdCardTags(mainSubWaypoints[i]); character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; if (character.Info.OrderData != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 7f78f853d..21dae52e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -8,19 +8,15 @@ namespace Barotrauma { internal partial class CampaignMetadata { - public CampaignMode Campaign { get; } - private readonly Dictionary data = new Dictionary(); - public CampaignMetadata(CampaignMode campaign) + public CampaignMetadata() { - Campaign = campaign; } - public CampaignMetadata(CampaignMode campaign, XElement element) + public void Load(XElement element) { - Campaign = campaign; - + data.Clear(); foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) @@ -59,6 +55,8 @@ namespace Barotrauma { DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); + SteamAchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true); + if (!data.ContainsKey(identifier)) { data.Add(identifier, value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 32dc12d72..b08da08a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,13 +1,16 @@ #nullable enable using Microsoft.Xna.Framework; -using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { public enum FactionAffiliation { - Affiliated, - Neutral + Positive, + Neutral, + Negative } class Faction @@ -25,21 +28,25 @@ namespace Barotrauma /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents /// /// - public FactionAffiliation GetPlayerAffiliationStatus() + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction) { - float affiliation = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - if (character.Info is not { } info) { continue; } + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } - affiliation *= 1f + info.GetSavedStatValue(StatTypes.Affiliation, Prefab.Identifier); + bool isHighest = true; + foreach (Faction otherFaction in factions) + { + if (otherFaction == faction || otherFaction.Reputation.Value < faction.Reputation.Value) { continue; } + + isHighest = false; + break; } - return affiliation switch - { - >= 1f => FactionAffiliation.Affiliated, - _ => FactionAffiliation.Neutral - }; + return isHighest ? FactionAffiliation.Positive : FactionAffiliation.Negative; + } + + public override string ToString() + { + return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; } } @@ -52,6 +59,54 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public class HireableCharacter + { + public readonly Identifier NPCSetIdentifier; + public readonly Identifier NPCIdentifier; + public readonly float MinReputation; + + public HireableCharacter(ContentXElement element) + { + NPCSetIdentifier = element.GetAttributeIdentifier("from", element.GetAttributeIdentifier("npcsetidentifier", Identifier.Empty)); + NPCIdentifier = element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("npcidentifier", Identifier.Empty)); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + } + } + + public ImmutableArray HireableCharacters; + + public class AutomaticMission + { + public readonly Identifier MissionTag; + public readonly LevelData.LevelType LevelType; + public readonly float MinReputation, MaxReputation; + public readonly float MinProbability, MaxProbability; + public readonly int MaxDistanceFromFactionOutpost; + public readonly bool DisallowBetweenOtherFactionOutposts; + + public AutomaticMission(ContentXElement element, string parentDebugName) + { + MissionTag = element.GetAttributeIdentifier("missiontag", Identifier.Empty); + LevelType = element.GetAttributeEnum("leveltype", LevelData.LevelType.LocationConnection); + MinReputation = element.GetAttributeFloat("minreputation", 0.0f); + MaxReputation = element.GetAttributeFloat("maxreputation", 0.0f); + if (MinReputation > MaxReputation) + { + DebugConsole.ThrowError($"Error in faction prefab \"{parentDebugName}\": MinReputation cannot be larger than MaxReputation."); + } + float probability = element.GetAttributeFloat("probability", 0.0f); + MinProbability = element.GetAttributeFloat("minprobability", probability); + MaxProbability = element.GetAttributeFloat("maxprobability", probability); + MaxDistanceFromFactionOutpost = element.GetAttributeInt(nameof(MaxDistanceFromFactionOutpost), int.MaxValue); + DisallowBetweenOtherFactionOutposts = element.GetAttributeBool(nameof(DisallowBetweenOtherFactionOutposts), false); + } + } + + public ImmutableArray AutomaticMissions; + + public bool StartOutpost { get; } + + public int MenuOrder { get; } /// @@ -69,38 +124,73 @@ namespace Barotrauma /// public int InitialReputation { get; } + public float ControlledOutpostPercentage { get; } + + public float SecondaryControlledOutpostPercentage { get; } + #if CLIENT public Sprite? Icon { get; private set; } + public Sprite? IconSmall { get; private set; } + public Sprite? BackgroundPortrait { get; private set; } +#endif public Color IconColor { get; } -#endif public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { MenuOrder = element.GetAttributeInt("menuorder", 0); + StartOutpost = element.GetAttributeBool("startoutpost", false); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); + ControlledOutpostPercentage = element.GetAttributeFloat("controlledoutpostpercentage", 0); + SecondaryControlledOutpostPercentage = element.GetAttributeFloat("secondarycontrolledoutpostpercentage", 0); Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); -#if CLIENT + + List hireableCharacters = new List(); + List automaticMissions = new List(); foreach (var subElement in element.Elements()) { - - if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) + var subElementId = subElement.NameAsIdentifier(); + if (subElementId == "icon") { IconColor = subElement.GetAttributeColor("color", Color.White); +#if CLIENT Icon = new Sprite(subElement); +#endif } - else if (subElement.Name.ToString().Equals("portrait", StringComparison.OrdinalIgnoreCase)) + else if (subElementId == "iconsmall") { +#if CLIENT + IconSmall = new Sprite(subElement); +#endif + } + else if (subElementId == "portrait") + { +#if CLIENT BackgroundPortrait = new Sprite(subElement); +#endif + } + else if (subElementId == "hireable") + { + hireableCharacters.Add(new HireableCharacter(subElement)); + } + else if (subElementId == "mission" || subElementId == "automaticmission") + { + automaticMissions.Add(new AutomaticMission(subElement, Identifier.ToString())); } } -#endif + HireableCharacters = hireableCharacters.ToImmutableArray(); + AutomaticMissions = automaticMissions.ToImmutableArray(); + } + + public override string ToString() + { + return $"{base.ToString()} ({Identifier})"; } public override void Dispose() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ed89cf0c7..5129e6a81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -1,17 +1,16 @@ using Microsoft.Xna.Framework; using System; -using System.Linq; namespace Barotrauma { class Reputation { public const float HostileThreshold = 0.2f; - public const float ReputationLossPerNPCDamage = 0.1f; - public const float ReputationLossPerStolenItemPrice = 0.01f; - public const float ReputationLossPerWallDamage = 0.1f; - public const float MinReputationLossPerStolenItem = 0.5f; - public const float MaxReputationLossPerStolenItem = 10.0f; + public const float ReputationLossPerNPCDamage = 0.05f; + public const float ReputationLossPerWallDamage = 0.05f; + public const float ReputationLossPerStolenItemPrice = 0.005f; + public const float MinReputationLossPerStolenItem = 0.05f; + public const float MaxReputationLossPerStolenItem = 1.0f; public Identifier Identifier { get; } public int MinReputation { get; } @@ -59,27 +58,34 @@ namespace Barotrauma Value = newReputation; } - public void AddReputation(float reputationChange) + public float GetReputationChangeMultiplier(float reputationChange) { if (reputationChange > 0f) { float reputationGainMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false); + reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } - reputationChange *= reputationGainMultiplier; + return reputationGainMultiplier; } else if (reputationChange < 0f) { float reputationLossMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier); + reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false); + reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; } - reputationChange *= reputationLossMultiplier; + return reputationLossMultiplier; } - Value += reputationChange; + return 1.0f; + } + + public void AddReputation(float reputationChange) + { + Value += reputationChange * GetReputationChangeMultiplier(reputationChange); } public readonly NamedEvent OnReputationValueChanged = new NamedEvent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index 795cb4680..98a2ebc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -69,7 +69,7 @@ namespace Barotrauma public Option RewardDistributionChanged; public Option BalanceChanged; - public WalletChangedData MergeInto(WalletChangedData other) + public readonly WalletChangedData MergeInto(WalletChangedData other) { other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); @@ -80,32 +80,20 @@ namespace Barotrauma static Option AddOptionalInt(Option a, Option b) { - return a switch - { - Some some1 => b switch - { - Some some2 => Option.Some(some1.Value + some2.Value), - None _ => Option.Some(some1.Value), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - None _ => b switch - { - Some some1 => Option.Some(some1.Value), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException(nameof(b)) - }, - _ => throw new ArgumentOutOfRangeException(nameof(a)) - }; + bool hasValue1 = a.TryUnwrap(out var value1); + bool hasValue2 = b.TryUnwrap(out var value2); + return hasValue1 + ? hasValue2 + ? Option.Some(value1 + value2) + : Option.Some(value1) + : hasValue2 + ? Option.Some(value2) + : Option.None; } static Option TurnToNoneIfZero(Option option) { - return option switch - { - Some s => s.Value == 0 ? Option.None() : option, - None _ => option, - _ => throw new ArgumentOutOfRangeException(nameof(option)) - }; + return option.Bind(i => i == 0 ? Option.None : Option.Some(i)); } } } @@ -223,12 +211,8 @@ namespace Barotrauma }; } - public string GetOwnerLogName() => Owner switch - { - Some { Value: var character } => character.Name, - None _ => "the bank", - _ => throw new ArgumentOutOfRangeException(nameof(Owner)) - }; + public string GetOwnerLogName() + => Owner.TryUnwrap(out var character) ? character.Name : "the bank"; partial void SettingsChanged(Option balanceChanged, Option rewardChanged); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ddfcd55e0..9db5d70b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -23,8 +23,6 @@ namespace Barotrauma public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; - //duration of the cinematic + credits at the end of the campaign - protected const float EndCinematicDuration = 240.0f; //duration of the camera transition at the end of a round protected const float EndTransitionDuration = 5.0f; //there can be no events before this time has passed during the 1st campaign round @@ -44,9 +42,10 @@ namespace Barotrauma public UpgradeManager UpgradeManager; public MedicalClinic MedicalClinic; - public List Factions; + private List factions; + public IReadOnlyList Factions => factions; - public CampaignMetadata CampaignMetadata; + public readonly CampaignMetadata CampaignMetadata; protected XElement petsElement; @@ -84,9 +83,9 @@ namespace Barotrauma public bool CheatsEnabled; - public const float HullRepairCostPerDamage = 0.5f, ItemRepairCostPerRepairDuration = 1.0f; + public const float HullRepairCostPerDamage = 0.1f, ItemRepairCostPerRepairDuration = 1.0f; public const int ShuttleReplaceCost = 1000; - public const int MaxHullRepairCost = 2000, MaxItemRepairCost = 2000; + public const int MaxHullRepairCost = 600, MaxItemRepairCost = 2000; protected bool wasDocked; @@ -141,10 +140,19 @@ namespace Barotrauma private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) { if (GameMain.NetworkMember == null) { return true; } - //allow managing if no-one with permissions is alive - return - GameMain.NetworkMember.ConnectedClients.Count == 1 || - GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + if (GameMain.NetworkMember.ConnectedClients.Count == 1) { return true; } + + if (GameMain.NetworkMember.GameStarted) + { + //allow managing if no-one with permissions is alive and in-game + return GameMain.NetworkMember.ConnectedClients.None(c => + c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && + (IsOwner(c) || c.HasPermission(permissions))); + } + else + { + return GameMain.NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions)); + } } protected CampaignMode(GameModePreset preset, CampaignSettings settings) @@ -157,26 +165,26 @@ namespace Barotrauma CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); + CampaignMetadata = new CampaignMetadata(); Identifier messageIdentifier = new Identifier("money"); #if CLIENT OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e => { - if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } + if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; } if (changed == 0) { return; } bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; - switch (e.Owner) + if (e.Owner.TryUnwrap(out var owner)) { - case Some { Value: var owner}: - owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); - break; - case None _ when IsSinglePlayer: - Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); - break; + owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); + } + else if (IsSinglePlayer) + { + Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); } string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString(); @@ -307,12 +315,12 @@ namespace Barotrauma return (int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost); } - public void InitCampaignData() + public void InitFactions() { - Factions = new List(); + factions = new List(); foreach (FactionPrefab factionPrefab in FactionPrefab.Prefabs) { - Factions.Add(new Faction(CampaignMetadata, factionPrefab)); + factions.Add(new Faction(CampaignMetadata, factionPrefab)); } } @@ -358,10 +366,9 @@ namespace Barotrauma currentLocation.DeselectMission(mission); } } - if (levelData.HasBeaconStation && !levelData.IsBeaconActive) { - var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconnoreward")).OrderBy(m => m.UintIdentifier); if (beaconMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); @@ -374,7 +381,7 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))).OrderBy(m => m.UintIdentifier); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("huntinggrounds")).OrderBy(m => m.UintIdentifier); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); @@ -400,15 +407,108 @@ namespace Barotrauma weights[i] = weight; } var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); - if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) + if (!Missions.Any(m => m.Prefab.Tags.Contains("huntinggrounds"))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } } + foreach (Faction faction in factions.OrderBy(f => f.Prefab.MenuOrder)) + { + foreach (var automaticMission in faction.Prefab.AutomaticMissions) + { + if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { continue; } + + if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.Type == LevelData.LevelType.LocationConnection) + { + if (Map.SelectedConnection.Locations.All(l => l.Faction != null && l.Faction != faction)) + { + continue; + } + } + if (automaticMission.MaxDistanceFromFactionOutpost < int.MaxValue) + { + if (!Map.LocationOrConnectionWithinDistance( + currentLocation, + automaticMission.MaxDistanceFromFactionOutpost, + loc => loc.Faction == faction)) + { + continue; + } + } + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed + TotalPassedLevels)); + if (levelData.Type != automaticMission.LevelType) { continue; } + float probability = + MathHelper.Lerp( + automaticMission.MinProbability, + automaticMission.MaxProbability, + MathUtils.InverseLerp(automaticMission.MinReputation, automaticMission.MaxReputation, faction.Reputation.Value)); + if (rand.NextDouble() < probability) + { + var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier); + if (missionPrefabs.Any()) + { + var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand); + if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate)) + { + continue; + } + if (automaticMission.LevelType == LevelData.LevelType.Outpost) + { + extraMissions.Add(missionPrefab.Instantiate(new Location[] { currentLocation, currentLocation }, Submarine.MainSub)); + } + else + { + extraMissions.Add(missionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); + } + } + } + } + } + } + if (levelData.Biome.IsEndBiome) + { + Identifier endMissionTag = Identifier.Empty; + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + int locationIndex = map.EndLocations.IndexOf(map.SelectedLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_locationconnection_" + locationIndex).ToIdentifier(); + } + } + else + { + int locationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (locationIndex > -1) + { + endMissionTag = ("endlevel_location_" + locationIndex).ToIdentifier(); + } + } + if (!endMissionTag.IsEmpty) + { + var endLevelMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(endMissionTag)).OrderBy(m => m.UintIdentifier); + if (endLevelMissionPrefabs.Any()) + { + Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand); + if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type)) + { + if (levelData.Type == LevelData.LevelType.LocationConnection) + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(map.SelectedConnection.Locations, Submarine.MainSub)); + } + else + { + extraMissions.Add(endLevelMissionPrefab.Instantiate(new Location[] { map.CurrentLocation, map.CurrentLocation }, Submarine.MainSub)); + } + } + } + } } } + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -503,13 +603,6 @@ namespace Barotrauma { if (leavingSub.AtEndExit) { - if (Map.EndLocation != null && - map.SelectedLocation == Map.EndLocation && - Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData)) - { - nextLevel = map.StartLocation.LevelData; - return TransitionType.End; - } if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null) { nextLevel = Level.Loaded.EndLocation.LevelData; @@ -554,8 +647,32 @@ namespace Barotrauma } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { - nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; - return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + int currentEndLocationIndex = map.EndLocations.IndexOf(map.CurrentLocation); + if (currentEndLocationIndex > -1) + { + if (currentEndLocationIndex == map.EndLocations.Count - 1) + { + //at the last end location, end of campaign + nextLevel = map.StartLocation?.LevelData; + return TransitionType.End; + } + else if (leavingSub.AtEndExit && currentEndLocationIndex < map.EndLocations.Count - 1) + { + //more end locations to go, progress to the next one + nextLevel = map.EndLocations[currentEndLocationIndex + 1]?.LevelData; + return TransitionType.ProgressToNextLocation; + } + else + { + nextLevel = null; + return TransitionType.None; + } + } + else + { + nextLevel = map.SelectedLocation == null ? null : map.SelectedConnection?.LevelData; + return nextLevel == null ? TransitionType.None : TransitionType.LeaveLocation; + } } else { @@ -579,9 +696,11 @@ namespace Barotrauma //TODO: ignore players who don't have the permission to trigger a transition between levels? var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer)); + CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1; + //allow leaving if inside an outpost, and the submarine is either docked to it or close enough - Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers); - Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers); + Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam); + Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam); int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 : leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost)); @@ -595,11 +714,11 @@ namespace Barotrauma return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd; - static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtStart(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { if (Level.Loaded.StartOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -609,26 +728,35 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtStartExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } } - static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers) + static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers, CharacterTeamType submarineTeam) { + if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) + { + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); + if (closestSub == null || !closestSub.AtEndExit) { return null; } + return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; + } //no "end" in outpost levels - if (Level.Loaded.Type == LevelData.LevelType.Outpost) { return null; } + if (Level.Loaded.Type == LevelData.LevelType.Outpost) + { + return null; + } if (Level.Loaded.EndOutpost == null) { - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -638,13 +766,13 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } //nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; } - Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam); if (closestSub == null || !closestSub.AtEndExit) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } @@ -765,8 +893,8 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); - location.Reset(); + location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty); + location.Reset(this); } Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); @@ -800,11 +928,56 @@ namespace Barotrauma protected virtual void EndCampaignProjSpecific() { } + /// + /// Returns a random faction based on their ControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions ControlledOutpostPercentage is less than 100% + public Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: false, allowEmpty); + } + + /// + /// Returns a random faction based on their SecondaryControlledOutpostPercentage + /// + /// If true, the method can return null if the sum of the factions SecondaryControlledOutpostPercentage is less than 100% + public Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty = true) + { + return GetRandomFaction(Factions, randSync, secondary: true, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Rand.RandSync randSync, bool secondary = false, bool allowEmpty = true) + { + return GetRandomFaction(factions, Rand.GetRNG(randSync), secondary, allowEmpty); + } + + public static Faction GetRandomFaction(IEnumerable factions, Random random, bool secondary = false, bool allowEmpty = true) + { + List factionsList = factions.OrderBy(f => f.Prefab.Identifier).ToList(); + List weights = factionsList.Select(f => secondary ? f.Prefab.SecondaryControlledOutpostPercentage : f.Prefab.ControlledOutpostPercentage).ToList(); + float percentageSum = weights.Sum(); + if (percentageSum < 100.0f && allowEmpty) + { + //chance of non-faction-specific outposts if percentage of controlled outposts is <100 + factionsList.Add(null); + weights.Add(100.0f - percentageSum); + } + return ToolBox.SelectWeightedRandom(factionsList, weights, random); + } + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } + if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) + { + if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation) + { + return false; + } + } if (!TryPurchase(client, characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; + characterInfo.Title = null; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); @@ -859,11 +1032,14 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && character.HumanPrefab is { Identifier: var merchantId }) { character.MerchantIdentifier = merchantId; + map.CurrentLocation?.GetStore(merchantId)?.SetMerchantFaction(character.Faction); } + character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && @@ -975,11 +1151,39 @@ namespace Barotrauma if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; } if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } - Location location = Map?.CurrentLocation; - if (location != null) + + if (npc.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.Faction) is Faction faction) { - location.Reputation.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); + faction.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); } + else + { + Location location = Map?.CurrentLocation; + if (location != null) + { + location.Reputation?.AddReputation(-attackResult.Damage * Reputation.ReputationLossPerNPCDamage); + } + } + } + + public Faction GetFaction(Identifier identifier) + { + return factions.Find(f => f.Prefab.Identifier == identifier); + } + + public float GetReputation(Identifier factionIdentifier) + { + var faction = + factionIdentifier == "location".ToIdentifier() ? + factions.Find(f => f == Map?.CurrentLocation?.Faction) : + factions.Find(f => f.Prefab.Identifier == factionIdentifier); + return faction?.Reputation?.Value ?? 0.0f; + } + + public FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier) + { + var faction = GetFaction(factionIdentifier); + return Faction.GetPlayerAffiliationStatus(faction); } public abstract void Save(XElement element); @@ -1097,7 +1301,7 @@ namespace Barotrauma var itemsToTransfer = new List<(Item item, Item container)>(); if (PendingSubmarineSwitch != null) { - var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); + var connectedSubs = currentSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); // Remove items from the old sub foreach (Item item in Item.ItemList) { @@ -1132,15 +1336,29 @@ namespace Barotrauma { // Load the new sub var newSub = new Submarine(PendingSubmarineSwitch); - var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player).ToHashSet(); - // Move the transferred items - List availableContainers = Item.ItemList - .Where(it => connectedSubs.Contains(it.Submarine) && it.HasTag("crate") && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed) - .Select(it => it.GetComponent()) - .Where(c => c != null) - .ToList(); + var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type == SubmarineType.Player); + WayPoint wp = WayPoint.WayPointList.FirstOrDefault(wp => wp.SpawnType == SpawnType.Cargo && connectedSubs.Contains(wp.Submarine)); + Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.FirstOrDefault(h => connectedSubs.Contains(h.Submarine) && !h.IsWetRoom); + if (spawnHull == null) + { + DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); + return; + } + // First move the cargo containers, so that we can reuse them + var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag("crate")); + foreach (var (item, oldContainer) in cargoContainers) + { + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); + item.CurrentHull = spawnHull; + item.Submarine = spawnHull.Submarine; + } + // Then move the other items + var cargoRooms = CargoManager.FindCargoRooms(newSub); + List availableContainers = CargoManager.FindReusableCargoContainers(connectedSubs).ToList(); foreach (var (item, oldContainer) in itemsToTransfer) { + if (cargoContainers.Contains((item, oldContainer))) { continue; } Item newContainer = null; item.Submarine = newSub; if (item.Container == null) @@ -1149,25 +1367,16 @@ namespace Barotrauma } if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false))) { - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub); - Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced(); - if (spawnHull == null) + var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); + if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) { - DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub."); - return; + Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); + item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); } - if (spawnHull != null) + else if (cargoContainer.Item.Submarine is Submarine containerSub) { - var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers); - if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false)) - { - Vector2 simPos = ConvertUnits.ToSimUnits(CargoManager.GetCargoPos(spawnHull, item.Prefab)); - item.SetTransform(simPos, 0.0f, findNewHull: false, setPrevTransform: false); - } - } - else - { - DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!"); + // Use the item's sub in case the sub consists of multiple linked subs. + item.Submarine = containerSub; } } string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 2377b386c..c07306e18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -11,11 +11,14 @@ namespace Barotrauma { public static CampaignSettings Empty => new CampaignSettings(element: null); +#if CLIENT + public static CampaignSettings CurrentSettings = new CampaignSettings(GameSettings.CurrentConfig.SavedCampaignSettings); +#endif public string Name => "CampaignSettings"; public const string LowerCaseSaveElementName = "campaignsettings"; - [Serialize("", IsPropertySaveable.Yes)] + [Serialize("Normal", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; [Serialize(true, IsPropertySaveable.Yes)] @@ -53,7 +56,6 @@ namespace Barotrauma return definition.GetInt(StartingBalanceAmount.ToIdentifier()); } return 8000; - } } @@ -65,7 +67,7 @@ namespace Barotrauma { return definition.GetFloat(Difficulty.ToIdentifier()); } - return 0; + return 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index a84b1a7c5..97521a5b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -53,7 +54,7 @@ namespace Barotrauma } } - private bool ValidateFlag(NetFlags flag) + private static bool ValidateFlag(NetFlags flag) { if (MathHelper.IsPowerOfTwo((int)flag)) { return true; } #if DEBUG @@ -105,9 +106,8 @@ namespace Barotrauma #endif } CampaignID = currentCampaignID; - CampaignMetadata = new CampaignMetadata(this); UpgradeManager = new UpgradeManager(this); - InitCampaignData(); + InitFactions(); } public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) @@ -190,11 +190,20 @@ namespace Barotrauma //map already created, update it //if we're not downloading the initial save file (LastSaveID > 0), //show notifications about location type changes - map.LoadState(subElement, LastSaveID > 0); + map.LoadState(this, subElement, LastSaveID > 0); } break; case "metadata": - CampaignMetadata = new CampaignMetadata(this, subElement); + var prevReputations = Factions.ToDictionary(k => k, v => v.Reputation.Value); + CampaignMetadata.Load(subElement); + foreach (var faction in Factions) + { + if (!MathUtils.NearlyEqual(prevReputations[faction], faction.Reputation.Value)) + { + faction.Reputation.OnReputationValueChanged?.Invoke(faction.Reputation); + Reputation.OnAnyReputationValueChanged.Invoke(faction.Reputation); + } + } break; case "upgrademanager": case "pendingupgrades": @@ -237,10 +246,8 @@ namespace Barotrauma }; } - CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); #if SERVER characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 83770b4a9..b1da17c3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; +using Barotrauma.Extensions; namespace Barotrauma { @@ -75,7 +76,10 @@ namespace Barotrauma get { if (Map != null) { return Map.CurrentLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } @@ -86,7 +90,10 @@ namespace Barotrauma get { if (Map != null) { return Map.SelectedLocation; } - if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); } + if (dummyLocations == null) + { + dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData); + } if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } @@ -248,13 +255,44 @@ namespace Barotrauma } } + public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null) + { + MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); + var forceParams = levelData?.ForceOutpostGenerationParams; + if (forceLocationType == null && + forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier())) + { + forceLocationType = + LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand); + } + var dummyLocations = CreateDummyLocations(rand, forceLocationType); + List factions = new List(); + foreach (var factionPrefab in FactionPrefab.Prefabs) + { + factions.Add(new Faction(new CampaignMetadata(), factionPrefab)); + } + foreach (var location in dummyLocations) + { + if (location.Type.HasOutpost) + { + location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false); + location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true); + } + } + return dummyLocations; + } + public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null) + { + return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType); + } + + private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null) { var dummyLocations = new Location[2]; - MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { - dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); + dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType); } return dummyLocations; } @@ -268,7 +306,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null) + public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -286,11 +324,6 @@ namespace Barotrauma } } } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) - { - Campaign!.TryPurchase(client, cost); - } - GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); Campaign!.PendingSubmarineSwitch = newSubmarine; Campaign!.TransferItemsOnSubSwitch = transferItems; } @@ -298,10 +331,11 @@ namespace Barotrauma public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, newSubmarine.Price)) { return; } + int price = newSubmarine.GetPrice(); + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { - GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); #if SERVER (Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList); @@ -395,6 +429,7 @@ namespace Barotrauma } } + GameMode!.AddExtraMissions(LevelData); foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation @@ -505,6 +540,8 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } + CharacterHUD.ClearBossProgressBars(); + RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); if (GameMode is not TutorialMode && GameMode is not TestGameMode) @@ -514,14 +551,15 @@ namespace Barotrauma { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); - if (missions.Count > 1) + var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage); + if (missionsToShow.Count() > 1) { string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); } else { - var mission = missions.FirstOrDefault(); + var mission = missionsToShow.FirstOrDefault(); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false); } } @@ -570,7 +608,6 @@ namespace Barotrauma if (GameMode != null && Submarine != null) { missions.Clear(); - GameMode.AddExtraMissions(LevelData); missions.AddRange(GameMode.Missions); GameMode.Start(); foreach (Mission mission in missions) @@ -613,7 +650,7 @@ namespace Barotrauma } } - CreatureMetrics.Instance.RecentlyEncountered.Clear(); + CreatureMetrics.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundDuration = 0.0f; @@ -629,7 +666,16 @@ namespace Barotrauma return; } - if (level.StartOutpost != null) + var originalSubPos = Submarine.WorldPosition; + var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == level.StartOutpost); + if (spawnPoint != null) + { + //pre-determine spawnpoint, just use it directly + Submarine.SetPosition(spawnPoint.WorldPosition); + Submarine.NeutralizeBallast(); + Submarine.EnableMaintainPosition(); + } + else if (level.StartOutpost != null) { //start by placing the sub below the outpost Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders(); @@ -684,7 +730,7 @@ namespace Barotrauma else { Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f); - Submarine.NeutralizeBallast(); + Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } } @@ -693,6 +739,7 @@ namespace Barotrauma Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } + } else { @@ -851,7 +898,7 @@ namespace Barotrauma GUI.PreventPauseMenuToggle = true; - if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null) + if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End) { GUI.ClearMessages(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); @@ -864,6 +911,7 @@ namespace Barotrauma TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); ObjectiveManager.ResetUI(); + CharacterHUD.ClearBossProgressBars(); #endif SteamAchievementManager.OnRoundEnded(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 43baec7c3..9e76c344f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -8,7 +8,7 @@ namespace Barotrauma public List AvailableCharacters { get; set; } public List PendingHires = new List(); - public const int MaxAvailableCharacters = 10; + public const int MaxAvailableCharacters = 6; public HireManager() { @@ -32,6 +32,24 @@ namespace Barotrauma var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); } + if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); } + if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); } + } + + private void GenerateFactionCharacters(FactionPrefab faction) + { + foreach (var character in faction.HireableCharacters) + { + HumanPrefab humanPrefab = NPCSet.Get(character.NPCSetIdentifier, character.NPCIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError($"Couldn't create a hireable for the location: character prefab \"{character.NPCIdentifier}\" not found in the NPC set \"{character.NPCSetIdentifier}\"."); + continue; + } + var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient); + characterInfo.MinReputationToHire = (faction.Identifier, character.MinReputation); + AvailableCharacters.Add(characterInfo); + } } public void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 2b40e6304..4d0a5186a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -14,6 +14,8 @@ namespace Barotrauma public enum NetworkHeader { REQUEST_AFFLICTIONS, + AFFLICTION_UPDATE, + UNSUBSCRIBE_ME, REQUEST_PENDING, ADD_PENDING, REMOVE_PENDING, @@ -295,6 +297,42 @@ namespace Barotrauma static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } + public static void OnAfflictionCountChanged(Character character) => + GameMain.GameSession?.Campaign?.MedicalClinic?.OnAfflictionCountChangedPrivate(character); + + private void OnAfflictionCountChangedPrivate(Character character) + { + if (character is not { CharacterHealth: { } health, Info: { } info }) { return; } + + ImmutableArray afflictions = GetAllAfflictions(health); + +#if CLIENT + if (GameMain.NetworkMember is null) + { + ui?.UpdateAfflictions(new NetCrewMember(info, afflictions)); + } + + ui?.UpdateCrewPanel(); +#elif SERVER + foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList()) + { + if (sub.Expiry < DateTimeOffset.Now) + { + afflictionSubscribers.Remove(sub); + continue; + } + + if (sub.Target == info) + { + ServerSend(new NetCrewMember(info, afflictions), + header: NetworkHeader.AFFLICTION_UPDATE, + deliveryMethod: DeliveryMethod.Unreliable, + targetClient: sub.Subscriber); + } + } +#endif + } + public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price); private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; @@ -330,7 +368,7 @@ namespace Barotrauma new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } + new NetAffliction { Identifier = AfflictionPrefab.DamageType, Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs new file mode 100644 index 000000000..386a1836b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/SlideshowPrefab.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + class SlideshowPrefab : Prefab + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public class Slide + { + public readonly LocalizedString Text; + public readonly Sprite Portrait; + + public readonly float FadeInDelay, FadeInDuration, FadeOutDuration; + public readonly float TextFadeInDelay, TextFadeInDuration; + + public Slide(ContentXElement element) + { + string text = element.GetAttributeString(nameof(Text), string.Empty); + Text = TextManager.Get(text).Fallback(text); + + FadeInDelay = element.GetAttributeFloat(nameof(FadeInDelay), 0.0f); + FadeInDuration = element.GetAttributeFloat(nameof(FadeInDuration), 2.0f); + FadeOutDuration = element.GetAttributeFloat(nameof(FadeOutDuration), 2.0f); + TextFadeInDelay = element.GetAttributeFloat(nameof(TextFadeInDelay), 2.0f); + TextFadeInDuration = element.GetAttributeFloat(nameof(TextFadeInDuration), 3.0f); + + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "portrait": + Portrait = new Sprite(subElement, lazyLoad: true); + break; + } + } + } + } + + public readonly ImmutableArray Slides; + + public SlideshowPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + List slides = new List(); + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "slide": + slides.Add(new Slide(subElement)); + break; + } + } + Slides = slides.ToImmutableArray(); + } + + public override void Dispose() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index f627284a2..d644c8267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -105,15 +105,21 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + + bool forceToSlot = subElement.GetAttributeBool("forcetoslot", false); + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) { - if (item != null && item.ParentInventory != this) + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: forceToSlot, slot: slot, onSpawned: (Item item) => { - string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - } - }); + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); + } } } @@ -172,21 +178,6 @@ namespace Barotrauma (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } - public bool CanBeAutoMovedToCorrectSlots(Item item) - { - if (item == null) { return false; } - foreach (var allowedSlot in item.AllowedSlots) - { - InvSlotType slotsFree = InvSlotType.None; - for (int i = 0; i < slots.Length; i++) - { - if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } - } - if (allowedSlot == slotsFree) { return true; } - } - return false; - } - public override void RemoveItem(Item item) { RemoveItem(item, tryEquipFromSameStack: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 3ef615e8a..29ac8fea9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -543,8 +543,8 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(doorBody == null); doorBody = GameMain.World.CreateRectangle( - DockingTarget.Door.Body.width, - DockingTarget.Door.Body.height, + DockingTarget.Door.Body.Width, + DockingTarget.Door.Body.Height, 1.0f, position); doorBody.UserData = DockingTarget.Door; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 96d89ddc3..3c4600b95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using FarseerPhysics.Dynamics; #if CLIENT using Barotrauma.Lights; #endif @@ -13,6 +14,10 @@ namespace Barotrauma.Items.Components { partial class Door : Pickable, IDrawableComponent, IServerSerializable { + private static readonly HashSet doorList = new HashSet(); + + public static IReadOnlyCollection DoorList { get { return doorList; } } + private Gap linkedGap; private bool isOpen; @@ -89,6 +94,9 @@ namespace Barotrauma.Items.Components public PhysicsBody Body { get; private set; } + //the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls) + public Fixture OutsideSubmarineFixture; + private float RepairThreshold { get { return item.GetComponent() == null ? 0.0f : item.MaxCondition; } @@ -162,9 +170,14 @@ namespace Barotrauma.Items.Components set { isOpen = value; - OpenState = (isOpen) ? 1.0f : 0.0f; + OpenState = isOpen ? 1.0f : 0.0f; } } + public bool IsClosed => !IsOpen; + + public bool IsFullyOpen => IsOpen && OpenState >= 1.0f; + + public bool IsFullyClosed => IsClosed && OpenState <= 0f; [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } @@ -226,6 +239,7 @@ namespace Barotrauma.Items.Components } IsActive = true; + doorList.Add(this); } public override void OnItemLoaded() @@ -369,6 +383,8 @@ namespace Barotrauma.Items.Components return; } + + bool isClosing = false; if ((!IsStuck && !IsJammed) || !isOpen) { @@ -394,11 +410,20 @@ namespace Barotrauma.Items.Components if (isClosing) { if (OpenState < 0.9f) { PushCharactersAway(); } + if (CheckSubmarinesInDoorWay()) + { + PredictedState = null; + isOpen = true; + } } else { bool wasEnabled = Body.Enabled; Body.Enabled = Impassable || openState < 1.0f; + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None; + } if (wasEnabled && !Body.Enabled && IsHorizontal) { //when opening a hatch, force characters above it to refresh the floor position @@ -442,6 +467,10 @@ namespace Barotrauma.Items.Components } PushCharactersAway(); } + if (OutsideSubmarineFixture != null && Body.Enabled) + { + OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith; + } #if CLIENT UpdateConvexHulls(); #endif @@ -462,10 +491,16 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } + + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Category.None; + } if (linkedGap != null) { linkedGap.Open = 1.0f; } + IsOpen = false; #if CLIENT if (convexHull != null) { convexHull.Enabled = false; } @@ -540,6 +575,36 @@ namespace Barotrauma.Items.Components convexHull?.Remove(); convexHull2?.Remove(); #endif + + doorList.Remove(this); + } + + private bool CheckSubmarinesInDoorWay() + { + if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; } + + Rectangle doorRect = item.WorldRect; + if (IsHorizontal) + { + doorRect.Width = (int)(item.Rect.Width * (1.0f - openState)); + } + else + { + doorRect.Height = (int)(item.Rect.Height * (1.0f - openState)); + } + + foreach (Submarine sub in Submarine.Loaded) + { + if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; } + Rectangle worldBorders = sub.Borders; + worldBorders.Location += sub.WorldPosition.ToPoint(); + if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; } + foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false)) + { + if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; } + } + } + return false; } bool itemPosErrorShown; @@ -563,7 +628,6 @@ namespace Barotrauma.Items.Components Vector2 currSize = IsHorizontal ? new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) : new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState)); - Vector2 simSize = ConvertUnits.ToSimUnits(currSize); foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index c77aa4a7c..28264f432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -67,11 +67,18 @@ namespace Barotrauma.Items.Components [Serialize(true, IsPropertySaveable.Yes, "")] public bool CanSpawn { get; set; } = true; + [Editable, Serialize(false, IsPropertySaveable.Yes, "")] + public bool PreloadCharacter { get; set; } + private float spawnTimer; private float? spawnTimerGoal; private int spawnedAmount = 0; + private Character? preloadedCharacter; + + private bool preloadInitiated; + public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -103,12 +110,21 @@ namespace Barotrauma.Items.Components } } } - - base.OnItemLoaded(); } public override void Update(float deltaTime, Camera cam) { + if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated) + { + SpawnCharacter(Vector2.Zero, onSpawn: (Character c) => + { + preloadedCharacter = c; + c.DisabledByEvent = true; + }); + preloadInitiated = true; + return; + } + base.Update(deltaTime, cam); item.SendSignal(CanSpawn ? "1" : "0", "state_out"); @@ -269,10 +285,18 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrWhiteSpace(SpeciesName)) { - Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); - Identifier species = allSpecies.GetRandomUnsynced(); - Entity.Spawner?.AddCharacterToSpawnQueue(species, pos); - spawnedAmount++; + if (preloadedCharacter != null) + { + preloadedCharacter.DisabledByEvent = false; + preloadedCharacter.TeleportTo(pos); + preloadedCharacter = null; + spawnedAmount++; + } + else + { + SpawnCharacter(pos); + spawnedAmount++; + } } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { @@ -291,5 +315,15 @@ namespace Barotrauma.Items.Components } } } + + private void SpawnCharacter(Vector2 pos, Action? onSpawn = null) + { + if (!string.IsNullOrWhiteSpace(SpeciesName)) + { + Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier species = allSpecies.GetRandomUnsynced(); + Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn); + } + } } } \ 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 b59784eeb..6620a203c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Items.Components Pusher = null; if (element.GetAttributeBool("blocksplayers", false)) { - Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, + Pusher = new PhysicsBody(item.body.Width, item.body.Height, item.body.Radius, item.body.Density, BodyType.Dynamic, Physics.CollisionItemBlocking, @@ -427,10 +427,11 @@ namespace Barotrauma.Items.Components return; } + //cannot hold and wear an item at the same time + //(unless the slot in which it's held and worn are equal - e.g. a suit with built-in tool or weapon on one hand) var wearable = item.GetComponent(); - if (wearable != null) + if (wearable != null && !wearable.AllowedSlots.SequenceEqual(allowedSlots)) { - //cannot hold and wear an item at the same time wearable.Unequip(character); } @@ -558,10 +559,16 @@ namespace Barotrauma.Items.Components public override bool OnPicked(Character picker) { +#if CLIENT if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { + if (!picker.Inventory.CanBeAutoMovedToCorrectSlots(item)) + { + picker.Inventory.FlashAllowedSlots(item, Color.Red); + } return false; } +#endif bool wasAttached = IsAttached; if (base.OnPicked(picker)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 4ad251c17..816ff3c9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -128,7 +128,7 @@ namespace Barotrauma.Items.Components if (body != null) { - trigger = new PhysicsBody(body.width, body.height, body.radius, + trigger = new PhysicsBody(body.Width, body.Height, body.Radius, body.Density, BodyType.Static, Physics.CollisionWall, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 9b926a0fa..3c4376228 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -446,7 +446,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 774bf603b..cf93558ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,9 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -98,6 +96,9 @@ namespace Barotrauma.Items.Components private set; } + private readonly IReadOnlySet suitableProjectiles; + + private enum ChargingState { Inactive, @@ -130,12 +131,11 @@ namespace Barotrauma.Items.Components // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; characterUsable = true; - + suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty()).ToHashSet(); if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload) { DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement."); } - InitProjSpecific(element); } @@ -143,7 +143,8 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ReloadTimer = Math.Min(reload, 1.0f); + //clamp above 1 to prevent rapid-firing by swapping weapons + ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer); IsActive = true; } @@ -259,7 +260,8 @@ namespace Barotrauma.Items.Components { Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; - float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); + float spread = GetSpread(character) * Projectile.GetSpreadFromPool(projectile.SpreadCounter); + var lastProjectile = LastProjectile; if (lastProjectile != projectile) { @@ -275,7 +277,7 @@ namespace Barotrauma.Items.Components { Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool(projectile.SpreadCounter)); } Item.RemoveContained(projectile.Item); } @@ -294,39 +296,41 @@ namespace Barotrauma.Items.Components public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { - var containedItems = item.OwnInventory?.AllItemsMod; - if (containedItems == null) { return null; } - - foreach (Item item in containedItems) + foreach (ItemContainer container in item.GetComponents()) { - if (item == null) { continue; } - Projectile projectile = item.GetComponent(); - if (projectile != null) { return projectile; } - } - - //projectile not found, see if one of the contained items contains projectiles - foreach (Item it in containedItems) - { - if (it == null) { continue; } - var containedSubItems = it.OwnInventory?.AllItemsMod; - if (containedSubItems == null) { continue; } - foreach (Item subItem in containedSubItems) + foreach (Item containedItem in container.Inventory.AllItemsMod) { - if (subItem == null) { continue; } - Projectile projectile = subItem.GetComponent(); - //apply OnUse statuseffects to the container in case it has to react to it somehow - //(play a sound, spawn more projectiles, reduce condition...) - if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + if (containedItem == null) { continue; } + Projectile projectile = containedItem.GetComponent(); + if (IsSuitableProjectile(projectile)) { return projectile; } + + //projectile not found, see if the contained item contains projectiles + var containedSubItems = containedItem.OwnInventory?.AllItemsMod; + if (containedSubItems == null) { continue; } + foreach (Item subItem in containedSubItems) { - subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); - } - if (projectile != null) { return projectile; } + if (subItem == null) { continue; } + Projectile subProjectile = subItem.GetComponent(); + //apply OnUse statuseffects to the container in case it has to react to it somehow + //(play a sound, spawn more projectiles, reduce condition...) + if (triggerOnUseOnContainers && subItem.Condition > 0.0f) + { + subItem.GetComponent()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f); + } + if (IsSuitableProjectile(subProjectile)) { return subProjectile; } + } } } - return null; } + private bool IsSuitableProjectile(Projectile projectile) + { + if (projectile?.Item == null) { return false; } + if (!suitableProjectiles.Any()) { return true; } + return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s)); + } + partial void LaunchProjSpecific(); } class AbilityRangedWeapon : AbilityObject, IAbilityItem diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 1451570d3..fd24d9673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -100,6 +100,9 @@ namespace Barotrauma.Items.Components [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } + [Serialize(false, IsPropertySaveable.No, description: "Should the tool ignore characters? Enabled e.g. for fire extinguisher.")] + public bool IgnoreCharacters { get; set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } @@ -313,7 +316,11 @@ namespace Barotrauma.Items.Components private readonly List fireSourcesInRange = new List(); private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List ignoredBodies) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair; + if (!IgnoreCharacters) + { + collisionCategories |= Physics.CollisionCharacter; + } //if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them if (statusEffectLists != null) @@ -703,7 +710,7 @@ namespace Barotrauma.Items.Components private float repairTimer; private Gap previousGap; private readonly float repairTimeOut = 5; - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (!(objective.OperateTarget is Gap leak)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 8b8ff6601..422152d7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -42,6 +42,8 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.5f; + public override bool Use(float deltaTime, Character character = null) { //actual throwing logic is handled in Update @@ -59,6 +61,7 @@ namespace Barotrauma.Items.Components base.Drop(dropper); throwState = ThrowState.None; throwAngle = ThrowAngleStart; + Item.ResetWaterDragCoefficient(); } public override void UpdateBroken(float deltaTime, Camera cam) @@ -97,6 +100,7 @@ namespace Barotrauma.Items.Components } item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; midAir = false; + Item.ResetWaterDragCoefficient(); } return; } @@ -188,6 +192,7 @@ namespace Barotrauma.Items.Components } item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); + item.WaterDragCoefficient = WaterDragCoefficient; item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); //disable platform collisions until the item comes back to rest again diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index c9eff7ea5..2bac47663 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -247,17 +247,10 @@ namespace Barotrauma.Items.Components [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } - /// /// Can be used by status effects or conditionals to the speed of the item /// - public float Speed - { - get - { - return item.Speed; - } - } + public float Speed => item.Speed; public readonly bool InheritStatusEffects; @@ -452,7 +445,7 @@ namespace Barotrauma.Items.Components public virtual void Drop(Character dropper) { } /// true if the operation was completed - public virtual bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 68c59aaee..5a90d80a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -12,20 +12,9 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { - class ActiveContainedItem - { - public readonly Item Item; - public readonly StatusEffect StatusEffect; - public readonly bool ExcludeBroken; - public readonly bool ExcludeFullCondition; - public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken, bool excludeFullCondition) - { - Item = item; - StatusEffect = statusEffect; - ExcludeBroken = excludeBroken; - ExcludeFullCondition = excludeFullCondition; - } - } + readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition); + + readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation); class SlotRestrictions { @@ -63,7 +52,9 @@ namespace Barotrauma.Items.Components public readonly ItemInventory Inventory; private readonly List activeContainedItems = new List(); - + + private readonly List drawableContainedItems = new List(); + private List[] itemIds; //how many items can be contained @@ -351,8 +342,6 @@ namespace Barotrauma.Items.Components public void OnItemContained(Item containedItem) { - item.SetContainedItemPositions(); - int index = Inventory.FindIndex(containedItem); if (index >= 0 && index < slotRestrictions.Length) { @@ -370,6 +359,14 @@ namespace Barotrauma.Items.Components } } + var relatedItem = FindContainableItem(containedItem); + drawableContainedItems.RemoveAll(d => d.Item == containedItem); + drawableContainedItems.Add(new DrawableContainedItem(containedItem, + Hide: relatedItem?.Hide ?? false, + ItemPos: relatedItem?.ItemPos, + Rotation: relatedItem?.Rotation ?? 0.0f)); + drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item))); + if (item.GetComponent() != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); @@ -384,6 +381,7 @@ namespace Barotrauma.Items.Components // Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it). SetContainedActive(true); } + item.SetContainedItemPositions(); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); OnContainedItemsChanged.Invoke(this); } @@ -396,6 +394,7 @@ namespace Barotrauma.Items.Components public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); + drawableContainedItems.RemoveAll(i => i.Item == containedItem); //deactivate if the inventory is empty IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); CharacterHUD.RecreateHudTextsIfFocused(item, containedItem); @@ -486,8 +485,8 @@ namespace Barotrauma.Items.Components item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter); item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); item.GetComponent()?.Equip(ownerCharacter); - autoInjectCooldown = AutoInjectInterval; } + autoInjectCooldown = AutoInjectInterval; } } @@ -512,10 +511,18 @@ namespace Barotrauma.Items.Components if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; } StatusEffect effect = activeContainedItem.StatusEffect; - if (effect.HasTargetType(StatusEffect.TargetType.This)) + if (effect.HasTargetType(StatusEffect.TargetType.This)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); - if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects); + } + if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character) + { + effect.Apply(ActionType.OnContaining, deltaTime, item, character); + } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { @@ -759,54 +766,50 @@ namespace Barotrauma.Items.Components int i = 0; Vector2 currentItemPos = transformedItemPos; - foreach (Item contained in Inventory.AllItems) + foreach (DrawableContainedItem contained in drawableContainedItems) { Vector2 itemPos = currentItemPos; - var relatedItem = FindContainableItem(contained); - if (relatedItem != null) + if (contained.ItemPos.HasValue) { - if (relatedItem.ItemPos.HasValue) + Vector2 pos = contained.ItemPos.Value; + if (item.body != null) { - Vector2 pos = relatedItem.ItemPos.Value; - if (item.body != null) + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + pos.X *= item.body.Dir; + itemPos = Vector2.Transform(pos, transform) + item.body.Position; + } + else + { + itemPos = pos; + // This code is aped based on above. Not tested. + if (item.FlippedX) { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - pos.X *= item.body.Dir; - itemPos = Vector2.Transform(pos, transform) + item.body.Position; + itemPos.X = -itemPos.X; + itemPos.X += item.Rect.Width; } - else + if (item.FlippedY) { - itemPos = pos; - // This code is aped based on above. Not tested. - if (item.FlippedX) - { - itemPos.X = -itemPos.X; - itemPos.X += item.Rect.Width; - } - if (item.FlippedY) - { - itemPos.Y = -itemPos.Y; - itemPos.Y -= item.Rect.Height; - } - itemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.RotationRad) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(item.RotationRad); - itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; - } + itemPos.Y = -itemPos.Y; + itemPos.Y -= item.Rect.Height; + } + itemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.RotationRad) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(item.RotationRad); + itemPos = Vector2.Transform(itemPos - item.Position, transform) + item.Position; } } - } + } - if (contained.body != null) + if (contained.Item.body != null) { try { Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); float rotation = itemRotation; - if (relatedItem != null && relatedItem.Rotation != 0) + if (contained.Rotation != 0) { - rotation = MathHelper.ToRadians(relatedItem.Rotation); + rotation = MathHelper.ToRadians(contained.Rotation); } if (item.body != null) { @@ -817,29 +820,29 @@ namespace Barotrauma.Items.Components { rotation += -item.RotationRad; } - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); - contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); - contained.body.UpdateDrawPosition(); + contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); + contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation); + contained.Item.body.UpdateDrawPosition(); } catch (Exception e) { DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); - GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name, + GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name, GameAnalyticsManager.ErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace()); } - contained.body.Submarine = item.Submarine; + contained.Item.body.Submarine = item.Submarine; } - contained.Rect = + contained.Item.Rect = new Rectangle( - (int)(itemPos.X - contained.Rect.Width / 2.0f), - (int)(itemPos.Y + contained.Rect.Height / 2.0f), - contained.Rect.Width, contained.Rect.Height); + (int)(itemPos.X - contained.Item.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Item.Rect.Height / 2.0f), + contained.Item.Rect.Width, contained.Item.Rect.Height); - contained.Submarine = item.Submarine; - contained.CurrentHull = item.CurrentHull; - contained.SetContainedItemPositions(); + contained.Item.Submarine = item.Submarine; + contained.Item.CurrentHull = item.CurrentHull; + contained.Item.SetContainedItemPositions(); i++; if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index d6ae48ec5..e3d29d09b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -116,7 +116,7 @@ namespace Barotrauma.Items.Components { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * voltageFactor; - float condition = item.Condition / item.MaxCondition; + float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); UpdateAITargets(noise); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 923623a3e..0401bda5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -235,7 +235,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { #if CLIENT if (GameMain.Client != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index cdf2c9298..3ecd2752f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -671,7 +671,7 @@ namespace Barotrauma.Items.Components return picker != null; } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 807c13d23..1beb6ee19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components private static readonly Dictionary> targetGroups = new Dictionary>(); - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (currentMode == Mode.Passive || !aiPingCheckPending) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 90922ec50..cc73b26fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -720,7 +720,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { character.AIController.SteeringManager.Reset(); if (objective.Override) @@ -813,7 +813,7 @@ namespace Barotrauma.Items.Components } } - sonar?.AIOperate(deltaTime, character, objective); + sonar?.CrewAIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 4c90945ba..45d2af075 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 23c9487a8..b7b475055 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -6,6 +6,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Voronoi2; @@ -13,6 +14,21 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent, IServerSerializable { + const int SpreadCounterWrapAround = 256; + + private static readonly ImmutableArray spreadPool; + static Projectile() + { + MTRandom random = new MTRandom(0); + spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray(); + } + + public static float GetSpreadFromPool(int seed) + { + if (seed < 0) { seed = -seed; } + return spreadPool[seed % SpreadCounterWrapAround]; + } + struct HitscanResult { public Fixture Fixture; @@ -41,10 +57,14 @@ namespace Barotrauma.Items.Components } } + public const float WaterDragCoefficient = 0.1f; + private readonly Queue impactQueue = new Queue(); private bool removePending; + public byte SpreadCounter { get; private set; } + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -280,6 +300,8 @@ namespace Barotrauma.Items.Components return; } + SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround); + InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); @@ -292,13 +314,13 @@ namespace Barotrauma.Items.Components switch (item.body.BodyShape) { case PhysicsBody.Shape.Circle: - Attack.DamageRange = item.body.radius; + Attack.DamageRange = item.body.Radius; break; case PhysicsBody.Shape.Capsule: - Attack.DamageRange = item.body.height / 2 + item.body.radius; + Attack.DamageRange = item.body.Height / 2 + item.body.Radius; break; case PhysicsBody.Shape.Rectangle: - Attack.DamageRange = new Vector2(item.body.width / 2.0f, item.body.height / 2.0f).Length(); + Attack.DamageRange = new Vector2(item.body.Width / 2.0f, item.body.Height / 2.0f).Length(); break; } Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange); @@ -359,7 +381,7 @@ namespace Barotrauma.Items.Components { #if SERVER launchRot = rotation; - Item.CreateServerEvent(this, new EventData(launch: true)); + Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1))); #endif } } @@ -383,8 +405,9 @@ namespace Barotrauma.Items.Components } else { - launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * GetSpreadFromPool(SpreadCounter)); } + SpreadCounter++; Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) @@ -401,8 +424,7 @@ namespace Barotrauma.Items.Components { item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); - DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); - System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); + DoLaunch(launchDir * modifiedLaunchImpulse); } } User = character; @@ -423,15 +445,26 @@ namespace Barotrauma.Items.Components } item.Drop(null, createNetworkEvent: false); + Item.WaterDragCoefficient = WaterDragCoefficient; launchPos = item.SimPosition; item.body.Enabled = true; - item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + if (item.body.BodyType == BodyType.Kinematic) + { + item.body.LinearVelocity = impulse; + } + else + { + impulse *= item.body.Mass; + item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f); + } item.body.FarseerBody.OnCollision += OnProjectileCollision; item.body.FarseerBody.IsBullet = true; + EnableProjectileCollisions(); + IsActive = true; if (stickJoint == null) { return; } @@ -447,6 +480,7 @@ namespace Barotrauma.Items.Components Vector2 simPositon = item.SimPosition; Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); + Item.WaterDragCoefficient = WaterDragCoefficient; item.body.Enabled = true; //set the velocity of the body because the OnProjectileCollision method @@ -505,6 +539,7 @@ namespace Barotrauma.Items.Components { var h = hits[i]; item.SetTransform(h.Point, rotation); + item.UpdateTransform(); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitCount++; @@ -560,6 +595,8 @@ namespace Barotrauma.Items.Components return true; } if (fixture.Body.UserData is VineTile) { return true; } + if (fixture.CollidesWith == Category.None) { return true; } + if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub @@ -611,6 +648,7 @@ namespace Barotrauma.Items.Components return -1; } if (fixture.Body.UserData is VineTile) { return -1; } + if (fixture.CollidesWith == Category.None) { return -1; } if (fixture.Body.UserData is Item item) { if (item.Condition <= 0) { return -1; } @@ -669,6 +707,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { + Item.ResetWaterDragCoefficient(); if (dropper != null) { DisableProjectileCollisions(); @@ -755,6 +794,7 @@ namespace Barotrauma.Items.Components { if (User != null && User.Removed) { User = null; return false; } if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; } + if (originalCollisionCategories == Category.None && originalCollisionTargets == Category.None) { return false; } //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -840,6 +880,13 @@ namespace Barotrauma.Items.Components } if (target.Body.UserData is Submarine sub) { + //hit an item in a different sub -> no need to ignore, we can process the impact with this info + //(if it wasn't, we'll move the projectile to that sub's coordinate space and let it hit what it hits there) + if (Launcher?.Submarine != sub && target.UserData is Item) + { + return false; + } + Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); @@ -886,7 +933,7 @@ namespace Barotrauma.Items.Components AttackResult attackResult = new AttackResult(); Character character = null; - if (target.Body.UserData is Submarine submarine) + if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item) { item.Move(-submarine.Position); item.Submarine = submarine; @@ -911,9 +958,11 @@ namespace Barotrauma.Items.Components if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); } if (limb.character != null) { character = limb.character; } } - else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item) is Item targetItem) + else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem) { if (targetItem.Removed) { return false; } + //hit the external collider of an item (turret?) of the same sub -> ignore + if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; } if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); @@ -925,7 +974,7 @@ namespace Barotrauma.Items.Components targetItem.Condition / targetItem.MaxCondition, emptyColor: GUIStyle.HealthBarColorLow, fullColor: GUIStyle.HealthBarColorHigh, - textTag: targetItem.Name); + textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty); } #endif } @@ -1091,10 +1140,14 @@ namespace Barotrauma.Items.Components private void EnableProjectileCollisions() { - item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; - if (!IgnoreProjectilesWhileActive) + if (item.body.CollisionCategories != Category.None) { + item.body.CollisionCategories = Physics.CollisionProjectile; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + } + if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive) + { + if (item.body.CollisionCategories == Category.None) { item.body.CollisionCategories = Physics.CollisionCharacter; } item.body.CollidesWith |= Physics.CollisionProjectile; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e5604f449..66e6e93e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,12 +1,11 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -17,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int updateDeteriorationCounter; + private const int UpdateDeteriorationInterval = 10; + private int prevSentConditionValue; private string conditionSignal; @@ -232,6 +234,7 @@ namespace Barotrauma.Items.Components public float RepairDegreeOfSuccess(Character character, List skills) { if (skills.Count == 0) { return 1.0f; } + if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; @@ -241,6 +244,7 @@ namespace Barotrauma.Items.Components public void RepairBoost(bool qteSuccess) { + if (CurrentFixer == null) { return; } if (qteSuccess) { item.Condition += RepairDegreeOfSuccess(CurrentFixer, requiredSkills) * 3 * (currentFixerAction == FixActions.Repair ? 1.0f : -1.0f); @@ -404,26 +408,11 @@ namespace Barotrauma.Items.Components #endif } } - if (!ShouldDeteriorate()) { return; } - if (item.Condition > 0.0f) + updateDeteriorationCounter++; + if (updateDeteriorationCounter >= UpdateDeteriorationInterval) { - if (deteriorationTimer > 0.0f) - { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) - { - deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); -#if SERVER - if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } -#endif - } - return; - } - - if (item.ConditionPercentage > MinDeteriorationCondition) - { - float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); - item.Condition -= deteriorationSpeed * deltaTime; - } + UpdateDeterioration(deltaTime * UpdateDeteriorationInterval); + updateDeteriorationCounter = 0; } return; } @@ -559,6 +548,30 @@ namespace Barotrauma.Items.Components } } + private void UpdateDeterioration(float deltaTime) + { + if (item.Condition <= 0.0f) { return; } + if (!ShouldDeteriorate()) { return; } + + if (deteriorationTimer > 0.0f) + { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + deteriorationTimer -= deltaTime * GetDeteriorationDelayMultiplier(); +#if SERVER + if (deteriorationTimer <= 0.0f) { item.CreateServerEvent(this); } +#endif + } + return; + } + + if (item.ConditionPercentage > MinDeteriorationCondition) + { + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; + } + } + private float GetMaxRepairConditionMultiplier(Character character) { if (character == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index f5e52d074..e07a31161 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -304,9 +304,12 @@ namespace Barotrauma.Items.Components } #if SERVER //make sure the clients know about the states of the checkboxes and text fields - if (item.Submarine == null || !item.Submarine.Loading) + if (customInterfaceElementList.Any()) { - item.CreateServerEvent(this); + if (item.Submarine == null || !item.Submarine.Loading) + { + item.CreateServerEvent(this); + } } #endif } @@ -326,7 +329,7 @@ namespace Barotrauma.Items.Components } foreach (StatusEffect effect in btnElement.StatusEffects) { - item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); + item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index da887ef93..a2dcaacfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; using Barotrauma.Networking; using Barotrauma.Extensions; #if CLIENT @@ -13,6 +12,9 @@ namespace Barotrauma.Items.Components partial class LightComponent : Powered, IServerSerializable, IDrawableComponent { private Color lightColor; + /// + /// The current brightness of the light source, affected by powerconsumption/voltage + /// private float lightBrightness; private float blinkFrequency; private float pulseFrequency, pulseAmount; @@ -94,7 +96,7 @@ namespace Barotrauma.Items.Components if (isOn == value && IsActive == value) { return; } IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); OnStateChanged(); } } @@ -174,7 +176,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (Light != null) { - Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent; + Light.Color = IsOn ? lightColor.Multiply(lightColorMultiplier) : Color.Transparent; } #endif } @@ -187,7 +189,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")] public bool AlphaBlend { get; @@ -214,7 +216,7 @@ namespace Barotrauma.Items.Components { if (base.IsActive == value) { return; } base.IsActive = isOn = value; - SetLightSourceState(value); + SetLightSourceState(value, value ? lightBrightness : 0.0f); } } @@ -245,7 +247,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); - SetLightSourceState(IsActive); + SetLightSourceState(IsActive, lightBrightness); turret = item.GetComponent(); #if CLIENT Drawable = AlphaBlend && Light.LightSprite != null; @@ -258,6 +260,12 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { +#if CLIENT + if (item.HiddenInGame) + { + Light.Enabled = false; + } +#endif CheckIfNeedsUpdate(); } @@ -273,7 +281,8 @@ namespace Barotrauma.Items.Components (statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) && (IsActiveConditionals == null || IsActiveConditionals.Count == 0)) { - SetLightSourceState(true); + lightBrightness = 1.0f; + SetLightSourceState(true, lightBrightness); SetLightSourceTransformProjSpecific(); base.IsActive = false; isOn = true; @@ -302,7 +311,8 @@ namespace Barotrauma.Items.Components #endif if (item.Container != null && item.GetRootInventoryOwner() is not Character) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } @@ -311,7 +321,8 @@ namespace Barotrauma.Items.Components PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) { - SetLightSourceState(false); + lightBrightness = 0.0f; + SetLightSourceState(false, 0.0f); return; } @@ -338,7 +349,7 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { - SetLightSourceState(false); + SetLightSourceState(false, 0.0f); } public override bool Use(float deltaTime, Character character = null) @@ -370,7 +381,7 @@ namespace Barotrauma.Items.Components { LightColor = XMLExtensions.ParseColor(signal.value, false); #if CLIENT - SetLightSourceState(Light.Enabled); + SetLightSourceState(Light.Enabled, lightColorMultiplier); #endif prevColorSignal = signal.value; } @@ -388,7 +399,7 @@ namespace Barotrauma.Items.Components target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness); } - partial void SetLightSourceState(bool enabled, float? brightness = null); + partial void SetLightSourceState(bool enabled, float brightness); public void SetLightSourceTransform() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 6ebfe3fc8..93527069d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 1d6188ebd..4f09c6764 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private const int MaxMessages = 60; - private List messageHistory = new List(MaxMessages); + private readonly List messageHistory = new List(MaxMessages); public LocalizedString DisplayedWelcomeMessage { @@ -67,6 +67,12 @@ namespace Barotrauma.Items.Components [Editable, Serialize(false, IsPropertySaveable.Yes, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } + [Serialize(false, IsPropertySaveable.No)] + public bool AutoHideScrollbar { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool WelcomeMessageDisplayed { get; set; } + private Color textColor = Color.LimeGreen; [Editable, Serialize("50,205,50,255", IsPropertySaveable.Yes, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] @@ -85,6 +91,15 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize("> ", IsPropertySaveable.Yes)] + public string LineStartSymbol { get; set; } + + [Editable, Serialize(false, IsPropertySaveable.No)] + public bool Readonly { get; set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool AutoScrollToBottom { get; set; } + private string OutputValue { get; set; } private string prevColorSignal; @@ -143,14 +158,14 @@ namespace Barotrauma.Items.Components #endif base.OnItemLoaded(); - if (!DisplayedWelcomeMessage.IsNullOrEmpty()) + if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed) { ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; - //remove welcome message if a game session is running so it doesn't reappear on successive rounds + //disable welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) { - welcomeMessage = null; + WelcomeMessageDisplayed = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index 012548826..85e995e5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -67,7 +67,13 @@ namespace Barotrauma.Items.Components { return GameMain.GameSession?.RoundDuration ?? 0.0f; } - } + } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool ApplyEffectsToCharactersInsideSub { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public bool MoveOutsideSub { get; set; } private readonly LevelTrigger.TriggererType triggeredBy; private readonly HashSet triggerers = new HashSet(); @@ -124,7 +130,7 @@ namespace Barotrauma.Items.Components PhysicsBody.FarseerBody.SetIsSensor(true); PhysicsBody.FarseerBody.OnCollision += OnCollision; PhysicsBody.FarseerBody.OnSeparation += OnSeparation; - RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius); + RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius); } public override void OnMapLoaded() @@ -137,7 +143,7 @@ namespace Barotrauma.Items.Components private bool OnCollision(Fixture sender, Fixture other, Contact contact) { if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } - if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; } + if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; } triggerers.Add(entity); return true; } @@ -162,6 +168,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (item.Submarine != null && MoveOutsideSub) + { + item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation); + item.CurrentHull = null; + item.Submarine = null; + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers); if (triggerOnce) @@ -201,6 +216,13 @@ namespace Barotrauma.Items.Components else if (triggerer is Submarine submarine) { LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); + foreach (Character c2 in Character.CharacterList) + { + if (c2.Submarine == submarine) + { + LevelTrigger.ApplyAttacks(attacks, c2, item.WorldPosition, deltaTime); + } + } } if (Math.Abs(Force) < 0.01f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 221ce222a..8455a3b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -59,8 +59,9 @@ namespace Barotrauma.Items.Components private float aiTargetingGraceTimer; private float aiFindTargetTimer; - private Character currentTarget; - const float aiFindTargetInterval = 5.0f; + private ISpatialEntity currentTarget; + private const float CrewAiFindTargetMaxInterval = 3.0f; + private const float CrewAIFindTargetMinInverval = 0.2f; private int currentLoaderIndex; @@ -73,6 +74,8 @@ namespace Barotrauma.Items.Components private List lightComponents; + private readonly bool isSlowTurret; + public float Rotation { get { return rotation; } @@ -317,6 +320,42 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, IsPropertySaveable.Yes, description:"Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."), Editable] + public bool AutoOperate { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."), Editable] + public float RandomAimAmount { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."), Editable] + public float RandomAimMinTime { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."), Editable] + public float RandomAimMaxTime { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"), Editable] + public bool RandomMovement { get; set; } + + [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"), Editable] + public bool AimDelay { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"), Editable] + public bool TargetCharacters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"), Editable] + public bool TargetMonsters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"), Editable] + public bool TargetHumans { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"), Editable] + public bool TargetSubmarines { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"), Editable] + public bool TargetItems { get; set; } + + [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."), Editable] + public Identifier FriendlyTag { get; private set; } + public Turret(Item item, ContentXElement element) : base(item, element) { @@ -346,6 +385,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = false; + isSlowTurret = item.HasTag("slowturret"); InitProjSpecific(element); } @@ -560,6 +600,11 @@ namespace Barotrauma.Items.Components } UpdateLightComponents(); + + if (AutoOperate) + { + UpdateAutoOperate(deltaTime); + } } public void UpdateLightComponents() @@ -658,13 +703,20 @@ namespace Barotrauma.Items.Components loaderBroken = true; continue; } - ItemContainer projectileContainer = linkedItem.GetComponent(); + if (tryUseProjectileContainer(linkedItem)) { break; } + } + tryUseProjectileContainer(item); + + bool tryUseProjectileContainer(Item containerItem) + { + ItemContainer projectileContainer = containerItem.GetComponent(); if (projectileContainer != null) { - linkedItem.Use(deltaTime, null); + containerItem.Use(deltaTime, null); projectiles = GetLoadedProjectiles(); - if (projectiles.Any()) { break; } + if (projectiles.Any()) { return true; } } + return false; } } if (projectiles.Count == 0 && !LaunchWithoutProjectile) @@ -895,17 +947,21 @@ namespace Barotrauma.Items.Components } private float waitTimer; - private float disorderTimer; + private float randomAimTimer; private float prevTargetRotation; private float updateTimer; private bool updatePending; - public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) - { - if (ai == null) { return; } + public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default) + { IsActive = true; + if (friendlyTag.IsEmpty) + { + friendlyTag = FriendlyTag; + } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -924,7 +980,7 @@ namespace Barotrauma.Items.Components updateTimer -= deltaTime; } - if (!ignoreDelay && waitTimer > 0) + if (AimDelay && waitTimer > 0) { waitTimer -= deltaTime; return; @@ -934,40 +990,48 @@ namespace Barotrauma.Items.Components float shootDistance = AIRange; ISpatialEntity target = null; float closestDist = shootDistance * shootDistance; - if (targetHumans || targetOtherCreatures) + if (TargetCharacters) { foreach (var character in Character.CharacterList) { - if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group == ai.Config.Entity) { continue; } - bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; - if (isHuman) - { - if (!targetHumans) - { - // Don't target humans if not defined to. - continue; - } - } - else if (!targetOtherCreatures) - { - // Don't target other creatures if not defined to. - continue; - } + if (!IsValidTarget(character)) { continue; } + float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority; + if (priority <= 0) { continue; } + if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; } float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + if (!CheckTurretAngle(character.WorldPosition)) { continue; } target = character; - closestDist = dist; + closestDist = dist / priority; } } - if (targetSubmarines) + if (TargetItems) + { + foreach (Item targetItem in Item.ItemList) + { + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDist) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + target = targetItem; + closestDist = dist / priority; + } + } + if (TargetSubmarines) { if (target == null || target.Submarine != null) { closestDist = maxDistance * maxDistance; foreach (Submarine sub in Submarine.Loaded) { - if (sub.Info.Type != SubmarineType.Player) { continue; } + if (sub == Item.Submarine) { continue; } + if (item.Submarine != null) + { + if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; } + } float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } closestSub = sub; @@ -981,34 +1045,41 @@ namespace Barotrauma.Items.Components if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } + // Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway. target = hull; closestDist = dist; } } } } - if (!ignoreDelay) + + if (target == null && RandomMovement) { - if (target == null) + // Random movement while there's no target + waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); + targetRotation = Rand.Range(minRotation, maxRotation); + updatePending = true; + return; + } + + if (AimDelay) + { + if (RandomAimAmount > 0) { - // Random movement - waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); - targetRotation = Rand.Range(minRotation, maxRotation); - updatePending = true; - return; - } - if (disorderTimer < 0) - { - // Random disorder - disorderTimer = Rand.Range(0f, 3f); - waitTimer = Rand.Range(0.25f, 1f); - targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f)); - updatePending = true; - return; - } - else - { - disorderTimer -= deltaTime; + if (randomAimTimer < 0) + { + // Random disorder or other flaw in the targeting. + randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); + waitTimer = Rand.Range(0.25f, 1f); + float randomAim = MathHelper.ToRadians(RandomAimAmount); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim)); + updatePending = true; + return; + } + else + { + randomAimTimer -= deltaTime; + } } } if (target == null) { return; } @@ -1043,11 +1114,11 @@ namespace Barotrauma.Items.Components start -= target.Submarine.SimPosition; end -= target.Submarine.SimPosition; Body transformedTarget = CheckLineOfSight(start, end); - shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines)); + shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)); } else { - shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines); + shoot = CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines); } if (shoot) { @@ -1055,7 +1126,7 @@ namespace Barotrauma.Items.Components } } - public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) + public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { @@ -1205,18 +1276,19 @@ namespace Barotrauma.Items.Components bool hadCurrentTarget = currentTarget != null; if (hadCurrentTarget) { - if (currentTarget.Removed || currentTarget.IsDead) + if (!IsValidTarget(currentTarget)) { currentTarget = null; + aiFindTargetTimer = CrewAIFindTargetMinInverval; } } - - if (aiFindTargetTimer <= 0.0f || currentTarget == null) + if (aiFindTargetTimer <= 0.0f) { foreach (Character enemy in Character.CharacterList) { - // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled) { continue; } + if (!IsValidTarget(enemy)) { continue; } + float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority; + if (priority <= 0) { continue; } if (character.Submarine != null) { if (enemy.Submarine == character.Submarine) { continue; } @@ -1233,30 +1305,53 @@ namespace Barotrauma.Items.Components // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } } + targetPos = enemy.WorldPosition; closestEnemy = enemy; - closestDistance = dist; + closestDistance = dist / priority; + currentTarget = closestEnemy; } - currentTarget = closestEnemy; - aiFindTargetTimer = aiFindTargetInterval; - } - else - { - closestEnemy = currentTarget; - } - - if (closestEnemy != null) - { - targetPos = closestEnemy.WorldPosition; - //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is - if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item)) + foreach (Item targetItem in Item.ItemList) { - targetPos = closestEnemy.CurrentHull.WorldPosition; + if (!IsValidTarget(targetItem)) { continue; } + float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority; + if (priority <= 0) { continue; } + float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (dist > closestDistance) { continue; } + if (dist > shootDistance * shootDistance) { continue; } + if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; } + targetPos = targetItem.WorldPosition; + closestDistance = dist / priority; + // Override the target character so that we can target the item instead. + closestEnemy = null; + currentTarget = targetItem; + } + if (currentTarget == null) + { + aiFindTargetTimer = CrewAIFindTargetMinInverval; + } + else + { + aiFindTargetTimer = CrewAiFindTargetMaxInterval; + } + } + else if (currentTarget != null) + { + targetPos = currentTarget.WorldPosition; + } + bool iceSpireSpotted = false; + // Adjust the target character position (limb or submarine) + if (currentTarget is Character targetCharacter) + { + //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is + if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item)) + { + targetPos = targetCharacter.CurrentHull.WorldPosition; } else { // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. float closestDist = closestDistance; - foreach (Limb limb in closestEnemy.AnimController.Limbs) + foreach (Limb limb in targetCharacter.AnimController.Limbs) { if (limb.IsSevered) { continue; } if (limb.Hidden) { continue; } @@ -1270,13 +1365,14 @@ namespace Barotrauma.Items.Components } if (closestDist > shootDistance * shootDistance) { - // Not close enough to shoot + // Not close enough to shoot. + currentTarget = null; closestEnemy = null; targetPos = null; } } } - else if (item.Submarine != null && Level.Loaded != null) + else if (targetPos == null && item.Submarine != null && Level.Loaded != null) { // Check ice spires shootDistance = AIRange * item.OffsetOnSelectedMultiplier; @@ -1286,50 +1382,49 @@ namespace Barotrauma.Items.Components if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; } foreach (var cell in wall.Cells) { - if (cell.DoesDamage) + if (!cell.DoesDamage) { continue; } + foreach (var edge in cell.Edges) { - foreach (var edge in cell.Edges) + Vector2 p1 = edge.Point1 + cell.Translation; + Vector2 p2 = edge.Point2 + cell.Translation; + Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); + if (!CheckTurretAngle(closestPoint)) { - Vector2 p1 = edge.Point1 + cell.Translation; - Vector2 p2 = edge.Point2 + cell.Translation; - Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); - if (!CheckTurretAngle(closestPoint)) + // The closest point can't be targeted -> get a point directly in front of the turret + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) { - // The closest point can't be targeted -> get a point directly in front of the turret - Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); - if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) - { - closestPoint = intersection; - if (!CheckTurretAngle(closestPoint)) { continue; } - } - else - { - continue; - } + closestPoint = intersection; + if (!CheckTurretAngle(closestPoint)) { continue; } } - float dist = Vector2.Distance(closestPoint, item.WorldPosition); - - //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell - closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); - - if (dist > AIRange + 1000) { continue; } - float dot = 0; - if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + else { - dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); - } - float minAngle = 0.5f; - if (dot < minAngle && dist > 1000) - { - // The sub is not moving towards the target and it's not very close to the turret either -> ignore continue; } - // Allow targeting farther when heading towards the spire (up to 1000 px) - dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); - if (dist > closestDistance) { continue; } - targetPos = closestPoint; - closestDistance = dist; } + float dist = Vector2.Distance(closestPoint, item.WorldPosition); + + //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell + closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1); + + if (dist > AIRange + 1000) { continue; } + float dot = 0; + if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero)) + { + dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); + } + float minAngle = 0.5f; + if (dot < minAngle && dist > 1000) + { + // The sub is not moving towards the target and it's not very close to the turret either -> ignore + continue; + } + // Allow targeting farther when heading towards the spire (up to 1000 px) + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); + if (dist > closestDistance) { continue; } + targetPos = closestPoint; + closestDistance = dist; + iceSpireSpotted = true; } } } @@ -1345,13 +1440,13 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) + if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman) { character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, identifier: "identifiedtargetspotted".ToIdentifier(), @@ -1364,17 +1459,17 @@ namespace Barotrauma.Items.Components minDurationBetweenSimilar: 5.0f); } } - else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) + else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName)) { character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } - character.AddEncounter(closestEnemy); + CreatureMetrics.AddEncounter(closestEnemy.SpeciesName); } character.AIController.SelectTarget(closestEnemy.AiTarget); } - else if (closestEnemy == null && character.IsOnPlayerTeam) + else if (iceSpireSpotted && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, identifier: "icespirespotted".ToIdentifier(), @@ -1437,7 +1532,55 @@ namespace Barotrauma.Items.Components return 0; } - private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + // Not exahustive, but helps to get rid of some code duplication + private static bool IsValidTarget(ISpatialEntity target) + { + if (target == null) { return false; } + if (target is Character targetCharacter) + { + if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0) + { + return false; + } + } + else if (target is Item targetItem) + { + if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.HiddenInGame) + { + return false; + } + if (targetItem.Submarine != null) + { + return false; + } + } + return true; + } + + private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag) + { + if (!friendlyTag.IsEmpty) + { + if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; } + } + bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName; + if (isHuman) + { + if (item.Submarine != null) + { + // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP). + return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans; + } + return TargetHumans; + } + else + { + // Shouldn't check the team here, because all the enemies are in the same team (None). + return TargetMonsters; + } + } + + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) { if (targetBody == null) { return false; } Character targetCharacter = null; @@ -1449,7 +1592,7 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null) + if (targetCharacter != null && !targetCharacter.Removed) { if (user != null) { @@ -1458,27 +1601,25 @@ namespace Barotrauma.Items.Components return false; } } - if (ai != null) + else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag)) { - if (targetCharacter.Params.Group == ai.Config.Entity) - { - return false; - } + // Note that Thalamus runs this even when AutoOperate is false. + return false; } } else { if (targetBody.UserData is ISpatialEntity e) { - if (e is Structure s && s.Indestructible) { return false; } - Submarine sub = e.Submarine ?? e as Submarine; + if (e is Structure { Indestructible: true }) { return false; } if (!targetSubmarines && e is Submarine) { return false; } - if (sub == null) { return false; } + Submarine sub = e.Submarine ?? e as Submarine; + if (sub == null) { return true; } if (sub == Item.Submarine) { return false; } if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; } if (sub.TeamID == Item.Submarine.TeamID) { return false; } } - else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) + else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true }) { // Hit something else, probably a level wall return false; @@ -1489,7 +1630,7 @@ namespace Barotrauma.Items.Components private Body CheckLineOfSight(Vector2 start, Vector2 end) { - var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile; Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, customPredicate: (Fixture f) => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index bd06a6ed3..fc8e6dd4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -527,14 +527,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (picker.Removed) + if (picker == null || picker.Removed) { IsActive = false; return; } - item.SetTransform(picker.SimPosition, 0.0f); - + //if the item is also being held, let the Holdable component control the position + if (item.GetComponent() is not { IsActive: true }) + { + item.SetTransform(picker.SimPosition, 0.0f); + } item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index b6eddc411..e9a617028 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -141,7 +141,18 @@ namespace Barotrauma } } if (items.Contains(item)) { return; } - items.Add(item); + + //keep lowest-condition items at the top of the stack + int index = 0; + for (int i = 0; i < items.Count; i++) + { + if (items[i].Condition > item.Condition) + { + break; + } + index++; + } + items.Insert(index, item); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 7a9a5c842..106553086 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -100,7 +100,18 @@ namespace Barotrauma private bool hasComponentsToDraw; public PhysicsBody body; - private float waterDragCoefficient; + private readonly float originalWaterDragCoefficient; + private float? overrideWaterDragCoefficient; + public float WaterDragCoefficient + { + get => overrideWaterDragCoefficient ?? originalWaterDragCoefficient; + set => overrideWaterDragCoefficient = value; + } + + /// + /// Removes the override value -> falls back to using the original value defined in the xml. + /// + public void ResetWaterDragCoefficient() => overrideWaterDragCoefficient = null; public readonly XElement StaticBodyConfig; @@ -143,6 +154,8 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; + public Action OnInteract; + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; @@ -424,8 +437,6 @@ namespace Barotrauma } } - public Color? HighlightColor; - /// /// Can be used by status effects or conditionals to check whether the item is contained inside something /// @@ -460,6 +471,8 @@ namespace Barotrauma } } + public Color? HighlightColor; + [Serialize("", IsPropertySaveable.Yes)] /// @@ -472,7 +485,8 @@ namespace Barotrauma { if (AiTarget != null) { - AiTarget.SonarLabel = !string.IsNullOrEmpty(value) && value.Length > 200 ? value.Substring(200) : value; + string trimmedStr = !string.IsNullOrEmpty(value) && value.Length > 250 ? value.Substring(250) : value; + AiTarget.SonarLabel = TextManager.Get(trimmedStr).Fallback(trimmedStr); } } } @@ -949,7 +963,7 @@ namespace Barotrauma { if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat)) { - DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")"); + DebugConsole.ThrowError("Invalid collision category in item \"" + Name + "\" (" + collisionCategoryStr + ")"); } else { @@ -988,6 +1002,7 @@ namespace Barotrauma case "infectedsprite": case "damagedinfectedsprite": case "swappableitem": + case "skillrequirementhint": break; case "staticbody": StaticBodyConfig = subElement; @@ -1056,8 +1071,7 @@ namespace Barotrauma if (body != null) { body.Submarine = submarine; - waterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", - GetComponent() != null || GetComponent() != null ? 0.1f : 1.0f); + originalWaterDragCoefficient = bodyElement.GetAttributeFloat("waterdragcoefficient", 5.0f); } //cache connections into a dictionary for faster lookups @@ -1656,7 +1670,7 @@ namespace Barotrauma if (effect.TargetSlot > -1) { - if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + if (!OwnInventory.GetItemsAt(effect.TargetSlot).Contains(containedItem)) { continue; } } hasTargets = true; @@ -1711,8 +1725,15 @@ namespace Barotrauma { targets.AddRange(character.AnimController.Limbs.ToList()); } + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null) + { + foreach (var characterLimb in character.AnimController.Limbs) + { + if (effect.targetLimbs.Contains(characterLimb.type)) { targets.Add(characterLimb); } + } + } } - if (effect.HasTargetType(StatusEffect.TargetType.Limb)) + if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb != null) { targets.Add(limb); } @@ -1727,7 +1748,7 @@ namespace Barotrauma { if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } - float damageAmount = attack.GetItemDamage(deltaTime); + float damageAmount = attack.GetItemDamage(deltaTime, Prefab.ItemDamageMultiplier); Condition -= damageAmount; if (damageAmount >= Prefab.OnDamagedThreshold) @@ -1975,7 +1996,10 @@ namespace Barotrauma if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty) { - UpdateTransform(); + if (body.CollisionCategories != Category.None) + { + UpdateTransform(); + } if (CurrentHull == null && Level.Loaded != null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { Spawner?.AddItemToRemoveQueue(this); @@ -1994,8 +2018,7 @@ namespace Barotrauma if (needsWaterCheck) { bool wasInWater = inWater; - inWater = IsInWater(); - bool waterProof = WaterProof; + inWater = IsInWater() && !WaterProof; if (inWater) { //the item has gone through the surface of the water @@ -2010,15 +2033,19 @@ namespace Barotrauma } Item container = this.Container; - while (!waterProof && container != null) + while (container != null) { - waterProof = container.WaterProof; + if (container.WaterProof) + { + inWater = false; + break; + } container = container.Container; } } if (hasWaterStatusEffects && condition > 0.0f) { - ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); + ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); } } else @@ -2144,7 +2171,7 @@ namespace Barotrauma Vector2 frontVel = body.FarseerBody.GetLinearVelocityFromLocalPoint(localFront); float speed = frontVel.Length(); - float drag = speed * speed * waterDragCoefficient * volume * Physics.NeutralDensity; + float drag = speed * speed * WaterDragCoefficient * volume * Physics.NeutralDensity; //very small drag on active projectiles to prevent affecting their trajectories much if (body.FarseerBody.IsBullet) { drag *= 0.1f; } Vector2 dragVec = -frontVel / speed * drag; @@ -2631,12 +2658,14 @@ namespace Barotrauma if (user == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected && !(ic is Door)) { selected = true; } + if (ic.CanBeSelected && ic is not Door) { selected = true; } } } if (!picked) { return false; } + OnInteract?.Invoke(); + if (user != null) { if (user.SelectedItem == this) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 761e84d24..a4a8fc521 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -1,16 +1,33 @@ -using Barotrauma.IO; +using Barotrauma.Extensions; +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Barotrauma.Extensions; using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { + readonly struct SkillRequirementHint + { + public readonly Identifier Skill; + public readonly float Level; + public readonly LocalizedString SkillName; + + public LocalizedString GetFormattedText(int skillLevel, string levelColorTag) => + $"{SkillName} {Level} (‖color:{levelColorTag}‖{skillLevel}‖color:end‖)"; + + public SkillRequirementHint(ContentXElement element) + { + Skill = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Level = element.GetAttributeFloat("level", 0); + SkillName = TextManager.Get("skillname." + Skill); + } + } + readonly struct DeconstructItem { public readonly Identifier ItemIdentifier; @@ -443,6 +460,8 @@ namespace Barotrauma //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. public ImmutableArray PreferredContainers { get; private set; } + public ImmutableArray SkillRequirementHints { get; private set; } + public SwappableItem SwappableItem { get; @@ -660,6 +679,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No)] public float ExplosionDamageMultiplier { get; private set; } + [Serialize(1f, IsPropertySaveable.No)] + public float ItemDamageMultiplier { get; private set; } + [Serialize(false, IsPropertySaveable.No)] public bool DamagedByProjectiles { get; private set; } @@ -772,6 +794,18 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")] public float BotPriority { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowNameInHealthBar { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description:"Should the bots shoot at this item with turret or not? Disabled by default.")] + public bool IsAITurretTarget { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with turrets? Defaults to 1. Distance to the target affects the decision making.")] + public float AITurretPriority { get; private set; } + + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")] + public float AISlowTurretPriority { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -874,6 +908,15 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, ConfigElement); LoadDescription(ConfigElement); + var skillRequirementHints = new List(); + foreach (var skillRequirementHintElement in ConfigElement.GetChildElements("SkillRequirementHint")) + { + skillRequirementHints.Add(new SkillRequirementHint(skillRequirementHintElement)); + } + if (skillRequirementHints.Any()) + { + SkillRequirementHints = skillRequirementHints.ToImmutableArray(); + } var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); @@ -1167,7 +1210,10 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + return + priceInfo is { CanBeBought: true } && + (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty && + (!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key)); } public bool CanBeBoughtFrom(Location location) @@ -1179,6 +1225,15 @@ namespace Barotrauma if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; } + if (priceInfo.MinReputation.Any()) + { + if (!priceInfo.MinReputation.Any(p => + location?.Faction?.Prefab.Identifier == p.Key || + location?.SecondaryFaction?.Prefab.Identifier == p.Key)) + { + continue; + } + } return true; } return false; @@ -1335,11 +1390,43 @@ namespace Barotrauma } public Identifier VariantOf { get; } - + public ItemPrefab ParentPrefab { get; set; } + public void InheritFrom(ItemPrefab parent) { - ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage); ParseConfigElement(parent); + + void CheckXML(XElement originalElement, XElement variantElement, XElement result) + { + if (result == null) { return; } + if (result.Name.ToIdentifier() == "RequiredItem" && + result.Parent?.Name.ToIdentifier() == "Fabricate") + { + int originalAmount = originalElement.GetAttributeInt("amount", 1); + Identifier originalIdentifier = originalElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (variantElement == null) + { + //if the variant defines some fabrication requirements, we probably don't want to inherit anything extra from the base item? + if (this.originalElement.GetChildElement("Fabricate")?.GetChildElement("RequiredItem") != null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the item inherits the fabrication requirement of x{originalAmount} \"{originalIdentifier}\" from the base item \"{parent.Identifier}\". " + + $"If this is not intentional, you can use empty elements in the item variant to remove any excess inherited fabrication requirements."); + } + return; + } + + Identifier resultIdentifier = result.GetAttributeIdentifier("identifier", Identifier.Empty); + if (originalAmount > 1 && variantElement.GetAttribute("amount") == null) + { + DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " + + $"the base item \"{parent.Identifier}\" requires x{originalAmount} \"{originalIdentifier}\" to fabricate. " + + $"The variant only overrides the required item, not the amount, resulting in a requirement of x{originalAmount} \"{resultIdentifier}\". "+ + "Specify the amount in the variant to fix this."); + } + } + } } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 41e4f333c..11e69f606 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -66,7 +66,7 @@ namespace Barotrauma /// /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. /// - public bool? Hide; + public bool Hide; public float Rotation; @@ -197,11 +197,14 @@ namespace Barotrauma bool isEmpty = parentItem.OwnInventory.IsEmpty(); if (RequireEmpty && !isEmpty) { return false; } if (MatchOnEmpty && isEmpty) { return true; } - foreach (Item contained in parentItem.ContainedItems) + foreach (var container in parentItem.GetComponents()) { - if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } - if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } - if (CheckContained(contained)) { return true; } + foreach (Item contained in container.Inventory.AllItems) + { + if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } + if ((!ExcludeBroken || contained.Condition > 0.0f) && (!ExcludeFullCondition || !contained.IsFullCondition) && MatchesItem(contained)) { return true; } + if (CheckContained(contained)) { return true; } + } } return false; } @@ -221,9 +224,9 @@ namespace Barotrauma new XAttribute("rotation", Rotation), new XAttribute("setactive", SetActive)); - if (Hide.HasValue) + if (Hide) { - element.Add(new XAttribute(nameof(Hide), Hide.Value)); + element.Add(new XAttribute(nameof(Hide), true)); } if (ItemPos.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 92effd495..59721d257 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -36,6 +36,10 @@ namespace Barotrauma public bool IdFreed { get; private set; } + /// + /// Unique, but non-persistent identifier. + /// Stays the same if the entities are created in the exactly same order, but doesn't persist e.g. between the rounds. + /// public readonly ushort ID; public virtual Vector2 SimPosition => Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b22a9280e..2ce9bd0e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -127,6 +127,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } + Attack.DamageMultiplier = 1.0f; float displayRange = Attack.Range; if (damageSource is Item sourceItem) { @@ -157,6 +158,10 @@ namespace Barotrauma Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f)); Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration); } + foreach (Item item in Item.ItemList) + { + item.GetComponent()?.RegisterExplosion(this, worldPosition); + } #endif if (displayRange < 0.1f) { return; } @@ -201,7 +206,7 @@ namespace Barotrauma powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } - static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange; + static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange; } if (itemRepairStrength > 0.0f) @@ -210,7 +215,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition); - if (distSqr > displayRangeSqr) continue; + if (distSqr > displayRangeSqr) { continue; } float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange; //repair repairable items @@ -266,7 +271,7 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { float distFactor = 1.0f - dist / displayRange; - float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier); Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -354,7 +359,7 @@ namespace Barotrauma if (affliction.DivideByLimbCount) { float limbCountFactor = distFactors.Count; - if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage") + if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType) { // Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor // Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing. @@ -396,9 +401,12 @@ namespace Barotrauma if (attack.StatusEffects != null && attack.StatusEffects.Any()) { attack.SetUser(attacker); - var statusEffectTargets = new List() { c, limb }; + var statusEffectTargets = new List(); foreach (StatusEffect statusEffect in attack.StatusEffects) { + statusEffectTargets.Clear(); + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffectTargets.Add(c); } + if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { statusEffectTargets.Add(limb); } statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets); statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 86f06c14f..96b5adda8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using MoonSharp.Interpreter; @@ -52,7 +53,6 @@ namespace Barotrauma //can ambient light get through the gap even if it's not open public bool PassAmbientLight; - //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub private Body outsideCollisionBlocker; @@ -64,8 +64,43 @@ namespace Barotrauma set { if (float.IsNaN(value)) { return; } - if (value > open) { openedTimer = 1.0f; } + if (value > open) + { + openedTimer = 1.0f; + } + if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) + { + if (value > open && value >= 1.0f) + { + InformWaypointsAboutGapState(this, open: true); + } + else if (value < open && open >= 1.0f) + { + InformWaypointsAboutGapState(this, open: false); + } + } open = MathHelper.Clamp(value, 0.0f, 1.0f); + + static void InformWaypointsAboutGapState(Gap gap, bool open) + { + foreach (var wp in WayPoint.WayPointList) + { + if (IsWaypointRightAboveGap(gap, wp)) + { + wp.OnGapStateChanged(open, gap); + } + } + } + + static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp) + { + if (wp.SpawnType != SpawnType.Path) { return false; } + if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; } + if (wp.Position.Y < gap.Rect.Top) { return false; } + if (wp.Position.X > gap.Rect.Right) { return false; } + if (wp.Position.X < gap.Rect.Left) { return false; } + return true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3752320d8..c604cc10d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1083,7 +1083,7 @@ namespace Barotrauma if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open - if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) + if ((g.ConnectedDoor.IsClosed && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index f48a4ec23..1aeb8ddd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -9,7 +9,7 @@ namespace Barotrauma Vector2 WorldPosition { get; } float Health { get; } - AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true); public readonly struct AttackEventData diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index 1144fc52b..2cc1d7c27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using System.Collections.Generic; using System.Collections.Immutable; @@ -13,11 +14,14 @@ namespace Barotrauma public readonly LocalizedString Description; public readonly bool IsEndBiome; + public readonly int EndBiomeLocationCount; + public readonly float MinDifficulty; private readonly float maxDifficulty; public float ActualMaxDifficulty => maxDifficulty; public float AdjustedMaxDifficulty => maxDifficulty - 0.1f; + public readonly ImmutableHashSet AllowedZones; private readonly SubmarineAvailability? submarineAvailability; @@ -41,6 +45,8 @@ namespace Barotrauma element.GetAttributeString("description", "")); IsEndBiome = element.GetAttributeBool("endbiome", false); + EndBiomeLocationCount = Math.Max(1, element.GetAttributeInt("endbiomelocationcount", 1)); + AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); MinDifficulty = element.GetAttributeFloat("MinDifficulty", 0); maxDifficulty = element.GetAttributeFloat("MaxDifficulty", 100); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 72bbace41..a6675f4a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -16,6 +16,11 @@ namespace Barotrauma { partial class Level : Entity, IServerSerializable { + public enum PlacementType + { + Top, Bottom + } + public enum EventType { SingleDestructibleWall, @@ -61,6 +66,7 @@ namespace Barotrauma [Flags] public enum PositionType { + None = 0, MainPath = 0x1, SidePath = 0x2, Cave = 0x4, @@ -68,7 +74,8 @@ namespace Barotrauma Wreck = 0x10, BeaconStation = 0x20, Abyss = 0x40, - AbyssCave = 0x80 + AbyssCave = 0x80, + Outpost = 0x100, } public struct InterestingPosition @@ -413,6 +420,9 @@ namespace Barotrauma get { return LevelData.Type; } } + + public bool IsEndBiome => LevelData.Biome != null && LevelData.Biome.IsEndBiome; + /// /// Is there a loaded level set and is it an outpost? /// @@ -451,6 +461,19 @@ namespace Barotrauma borders = new Rectangle(Point.Zero, levelData.Size); } + public bool ShouldSpawnCrewInsideOutpost() + { + if (StartOutpost != null && + Type == LevelData.LevelType.Outpost && + (StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && + StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) + { + var reputation = GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation; + return reputation == null || reputation.NormalizedValue >= Reputation.HostileThreshold; + } + return false; + } + public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) { Debug.Assert(levelData.Biome != null); @@ -482,11 +505,8 @@ namespace Barotrauma EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); - if (LevelData.ForceOutpostGenerationParams == null) - { - StartLocation = startLocation; - EndLocation = endLocation; - } + StartLocation = startLocation; + EndLocation = endLocation; GenerateEqualityCheckValue(LevelGenStage.GenStart); SetEqualityCheckValue(LevelGenStage.LevelGenParams, unchecked((int)GenerationParams.UintIdentifier)); @@ -889,6 +909,12 @@ namespace Barotrauma // remove unnecessary cells and create some holes at the bottom of the level //---------------------------------------------------------------------------------- + if (GenerationParams.NoLevelGeometry) + { + cells.ForEach(c => c.CellType = CellType.Removed); + cells.Clear(); + } + cells = cells.Except(pathCells).ToList(); //remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad cells.ForEachMod(c => @@ -1687,14 +1713,22 @@ namespace Barotrauma foreach (VoronoiCell cell in closeCells) { bool tooClose = false; - foreach (GraphEdge edge in cell.Edges) - { - if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || - Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || - MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + + if (cell.IsPointInsideAABB(position, margin: minDistance)) + { + tooClose = true; + } + else + { + foreach (GraphEdge edge in cell.Edges) { - tooClose = true; - break; + if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || + Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || + MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) + { + tooClose = true; + break; + } } } if (tooClose) { tooCloseCells.Add(cell); } @@ -3234,7 +3268,8 @@ namespace Barotrauma { suitablePositions.RemoveAll(p => !filter(p)); } - if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) + if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath) || positionType.HasFlag(PositionType.Abyss) || + positionType.HasFlag(PositionType.Cave) || positionType.HasFlag(PositionType.AbyssCave)) { suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2())); } @@ -3399,8 +3434,7 @@ namespace Barotrauma bool closeEnough = false; foreach (VoronoiCell cell in wall.Cells) { - if (Math.Abs(cell.Center.X - worldPos.X) < (searchDepth + 1) * GridCellSize && - Math.Abs(cell.Center.Y - worldPos.Y) < (searchDepth + 1) * GridCellSize) + if (cell.IsPointInsideAABB(worldPos, margin: (searchDepth + 1) * GridCellSize / 2)) { closeEnough = true; break; @@ -3553,6 +3587,13 @@ namespace Barotrauma var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); + SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) + { + Type = type + }; + + //place downwards by default + var placement = info.BeaconStationInfo?.Placement ?? PlacementType.Bottom; // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); @@ -3573,7 +3614,7 @@ namespace Barotrauma attemptsLeft--; if (TryGetSpawnPoint(out spawnPoint)) { - success = TryPositionSub(subBorders, subName, ref spawnPoint); + success = TryPositionSub(subBorders, subName, placement, ref spawnPoint); if (success) { break; @@ -3594,10 +3635,6 @@ namespace Barotrauma { Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) - { - Type = type - }; Submarine sub = new Submarine(info); if (type == SubmarineType.Wreck) { @@ -3647,10 +3684,10 @@ namespace Barotrauma return null; } - bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint) - { + bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint) + { positions.Add(spawnPoint); - bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint); + bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint); positions.Add(spawnPoint); bool leftSideBlocked = IsSideBlocked(subBorders, false); @@ -3658,21 +3695,21 @@ namespace Barotrauma int step = 5; if (rightSideBlocked && !leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (leftSideBlocked && !rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else if (!bottomFound) { if (!leftSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, -step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step); } else if (!rightSideBlocked) { - bottomFound = TryMove(subBorders, ref spawnPoint, step); + bottomFound = TryMove(subBorders, placement, ref spawnPoint, step); } else { @@ -3702,14 +3739,14 @@ namespace Barotrauma } return !isBlocked && bottomFound; - bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount) + bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount) { float maxMovement = 5000; float totalAmount = 0; - bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); while (!IsSideBlocked(subBorders, amount > 0)) { - foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + foundBottom = TryRaycast(subBorders, placement, ref spawnPoint); totalAmount += amount; spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); if (Math.Abs(totalAmount) > maxMovement) @@ -3738,7 +3775,7 @@ namespace Barotrauma return false; } - bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint) + bool TryRaycast(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint) { // Shoot five rays and pick the highest hit point. int rayCount = 5; @@ -3764,16 +3801,18 @@ namespace Barotrauma break; } var simPos = ConvertUnits.ToSimUnits(rayStart); - var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), - customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body), + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, placement == PlacementType.Bottom ? -1 : Size.Y + 1), + customPredicate: f => f.Body == TopBarrier || f.Body == BottomBarrier || (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body)), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); if (body != null) { - positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2); + positions[i] = + ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + + new Vector2(0, subBorders.Height / 2 * (placement == PlacementType.Bottom ? 1 : -1)); hit = true; } } - float highestPoint = positions.Max(p => p.Y); + float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(p => p.Y); spawnPoint = new Vector2(spawnPoint.X, highestPoint); return hit; } @@ -3953,8 +3992,18 @@ namespace Barotrauma if (LevelData.OutpostGenerationParamsExist) { Location location = i == 0 ? StartLocation : EndLocation; - OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ?? - LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient); + OutpostGenerationParams outpostGenerationParams = null; + if (LevelData.ForceOutpostGenerationParams != null) + { + outpostGenerationParams = LevelData.ForceOutpostGenerationParams; + } + else + { + outpostGenerationParams = + LevelData.ForceOutpostGenerationParams ?? + LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient); + } + LocationType locationType = location?.Type; if (locationType == null) { @@ -4030,52 +4079,70 @@ namespace Barotrauma } } - DockingPort outpostPort = null; - closestDistance = float.MaxValue; - foreach (DockingPort port in DockingPort.List) + Vector2 spawnPos; + if (GenerationParams.ForceOutpostPosition != Vector2.Zero) { - if (port.IsHorizontal || port.Docked) { continue; } - if (port.Item.Submarine != outpost) { continue; } - //the outpost port has to be at the bottom of the outpost - if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } - float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); - if (dist < closestDistance) + spawnPos = new Vector2(Size.X * GenerationParams.ForceOutpostPosition.X, Size.Y * GenerationParams.ForceOutpostPosition.Y); + } + else + { + DockingPort outpostPort = null; + closestDistance = float.MaxValue; + foreach (DockingPort port in DockingPort.List) { - outpostPort = port; - closestDistance = dist; + if (port.IsHorizontal || port.Docked) { continue; } + if (port.Item.Submarine != outpost) { continue; } + //the outpost port has to be at the bottom of the outpost + if (port.Item.WorldPosition.Y > outpost.WorldPosition.Y) { continue; } + float dist = Math.Abs(port.Item.WorldPosition.X - outpost.WorldPosition.X); + if (dist < closestDistance) + { + outpostPort = port; + closestDistance = dist; + } } - } - float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; - //don't try to compensate if the port is very far from the sub's center of mass - if (Math.Abs(subDockingPortOffset) > 5000.0f) - { - subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; - DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); - } - - float? outpostDockingPortOffset = null; - if (outpostPort != null) - { - outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; - //don't try to compensate if the port is very far from the outpost's center of mass - if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + float subDockingPortOffset = subPort == null ? 0.0f : subPort.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X; + //don't try to compensate if the port is very far from the sub's center of mass + if (Math.Abs(subDockingPortOffset) > 5000.0f) { - outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + + float? outpostDockingPortOffset = null; + if (outpostPort != null) + { + outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; + //don't try to compensate if the port is very far from the outpost's center of mass + if (Math.Abs(outpostDockingPortOffset.Value) > 5000.0f) + { + outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset.Value, -5000.0f, 5000.0f); + string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + DebugConsole.NewMessage(warningMsg, Color.Orange); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); + } + } + + spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); + if (Type == LevelData.LevelType.Outpost) + { + spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); } } - Vector2 spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1); - if (Type == LevelData.LevelType.Outpost) - { - spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); - } outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); + + foreach (WayPoint wp in WayPoint.WayPointList) + { + if (wp.Submarine == outpost && wp.SpawnType != SpawnType.Path) + { + PositionsOfInterest.Add(new InterestingPosition(wp.WorldPosition.ToPoint(), PositionType.Outpost, outpost)); + } + } + if ((i == 0) == !Mirrored) { StartOutpost = outpost; @@ -4094,13 +4161,12 @@ namespace Barotrauma outpost.Info.Name = EndLocation.Name; } } - } } private void CreateBeaconStation() { - if (!LevelData.HasBeaconStation) { return; } + if (!LevelData.HasBeaconStation && string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { return; } var beaconStationFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) .OrderBy(f => f.UintIdentifier).ToList(); @@ -4111,27 +4177,40 @@ namespace Barotrauma } var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon); - for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + ContentFile contentFile = null; + if (!string.IsNullOrEmpty(GenerationParams.ForceBeaconStation)) { - var beaconStationFile = beaconStationFiles[i]; - var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); - Debug.Assert(matchingInfo != null); - if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + contentFile = beaconStationFiles.FirstOrDefault(f => f.Path == GenerationParams.ForceBeaconStation); + if (contentFile == null) { - if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) - { - beaconStationFiles.RemoveAt(i); - } + DebugConsole.ThrowError($"Failed to find the beacon station \"{GenerationParams.ForceBeaconStation}\". Using a random one instead..."); } } - if (beaconStationFiles.None()) - { - DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); - return; - } - var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); - string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); + if (contentFile == null) + { + for (int i = beaconStationFiles.Count - 1; i >= 0; i--) + { + var beaconStationFile = beaconStationFiles[i]; + var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value); + Debug.Assert(matchingInfo != null); + if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo) + { + if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty) + { + beaconStationFiles.RemoveAt(i); + } + } + } + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!"); + return; + } + contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); + } + + string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); if (BeaconStation == null) { @@ -4195,7 +4274,7 @@ namespace Barotrauma { bool allowDisconnectedWires = true; bool allowDamagedWalls = true; - if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info) + if (BeaconStation?.Info?.BeaconStationInfo is BeaconStationInfo info) { allowDisconnectedWires = info.AllowDisconnectedWires; allowDamagedWalls = info.AllowDamagedWalls; @@ -4346,7 +4425,7 @@ namespace Barotrauma corpse.AnimController.FindHull(worldPos, setSubmarine: true); corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; - selectedPrefab.GiveItems(corpse, wreck); + selectedPrefab.GiveItems(corpse, wreck, sp); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200)); bool applyBurns = Rand.Value() < 0.1f; @@ -4377,7 +4456,6 @@ namespace Barotrauma } } corpse.CharacterHealth.ForceUpdateVisuals(); - corpse.GiveIdCardTags(sp); bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index f926af7ca..090259ff9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -24,7 +24,7 @@ namespace Barotrauma public readonly Biome Biome; - public readonly LevelGenerationParams GenerationParams; + public LevelGenerationParams GenerationParams { get; private set; } public bool HasBeaconStation; public bool IsBeaconActive; @@ -60,12 +60,12 @@ namespace Barotrauma /// /// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of . /// - public readonly List EventHistory = new List(); + public readonly List EventHistory = new List(); /// /// Events that have already triggered in this level and can never trigger again. . /// - public readonly List NonRepeatableEvents = new List(); + public readonly List NonRepeatableEvents = new List(); /// /// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. . @@ -150,10 +150,10 @@ namespace Barotrauma } string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); - EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); - NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } @@ -163,7 +163,7 @@ namespace Barotrauma /// public LevelData(LocationConnection locationConnection) { - Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; + Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; Difficulty = locationConnection.Difficulty; @@ -196,9 +196,9 @@ namespace Barotrauma /// /// Instantiates level data using the properties of the location /// - public LevelData(Location location, float difficulty) + public LevelData(Location location, Map map, float difficulty) { - Seed = location.BaseName; + Seed = location.BaseName + map.Locations.IndexOf(location); Biome = location.Biome; Type = LevelType.Outpost; Difficulty = difficulty; @@ -254,14 +254,22 @@ namespace Barotrauma return levelData; } + public void ReassignGenerationParams(string seed) + { + GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier); + } public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any(); - public static IEnumerable GetSuitableOutpostGenerationParams(Location location) + public static IEnumerable GetSuitableOutpostGenerationParams(Location location, LevelData levelData) { - var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + var suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); if (!suitableParams.Any()) { - suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + suitableParams = OutpostGenerationParams.OutpostParams + .Where(p => p.LevelType == null || levelData.Type == p.LevelType) + .Where(p => location == null || !p.AllowedLocationTypes.Any()); if (!suitableParams.Any()) { DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); @@ -305,11 +313,11 @@ namespace Barotrauma { if (EventHistory.Any()) { - newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("eventhistory", string.Join(',', EventHistory))); } if (NonRepeatableEvents.Any()) { - newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents.Select(p => p.Identifier)))); + newElement.Add(new XAttribute("nonrepeatableevents", string.Join(',', NonRepeatableEvents))); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 498702fa1..3766dd230 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -67,7 +67,7 @@ namespace Barotrauma set; } - [Serialize(1.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(100.0f, IsPropertySaveable.Yes, "If there are multiple level generation parameters available for a level in a given biome, their commonness determines how likely it is for one to get selected."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float Commonness { get; @@ -116,6 +116,13 @@ namespace Barotrauma set; } + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] + public Color WaterParticleColor + { + get; + set; + } + private Vector2 startPosition; [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition @@ -142,6 +149,19 @@ namespace Barotrauma } } + private Vector2 forceOutpostPosition; + [Serialize("0,0", IsPropertySaveable.Yes, "Position of the outpost (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner). If set to 0,0, the outpost is placed in a suitable position automatically."), Editable(DecimalCount = 2)] + public Vector2 ForceOutpostPosition + { + get { return forceOutpostPosition; } + set + { + forceOutpostPosition = new Vector2( + MathHelper.Clamp(value.X, 0.0f, 1.0f), + MathHelper.Clamp(value.Y, 0.0f, 1.0f)); + } + } + [Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] public bool CreateHoleNextToEnd { @@ -156,6 +176,13 @@ namespace Barotrauma set; } + [Serialize(false, IsPropertySaveable.Yes, ""), Editable] + public bool NoLevelGeometry + { + get; + set; + } + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { @@ -478,14 +505,14 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + // TODO: Move the wreck parameters under a separate class? +#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } [Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxWreckCount { get; set; } - // TODO: Move the wreck parameters under a separate class? -#region Wreck parameters [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MinCorpseCount { get; set; } @@ -503,7 +530,10 @@ namespace Barotrauma [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMaxWaterPercentage { get; set; } -#endregion + #endregion + + [Serialize("", IsPropertySaveable.Yes)] + public string ForceBeaconStation { get; set; } [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability @@ -519,6 +549,14 @@ namespace Barotrauma private set { waterParticleScale = Math.Max(value, 0.01f); } } + private Vector2 waterParticleVelocity; + [Serialize("0,10", IsPropertySaveable.Yes, description: "How fast the water particle texture scrolls."), Editable] + public Vector2 WaterParticleVelocity + { + get { return waterParticleVelocity; } + private set { waterParticleVelocity = value; } + } + [Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallTextureSize { @@ -533,6 +571,34 @@ namespace Barotrauma private set; } + [Serialize("0,0", IsPropertySaveable.Yes), Editable] + public Vector2 FlashInterval + { + get; + set; + } + + [Serialize("0,0,0,0", IsPropertySaveable.Yes), Editable] + public Color FlashColor + { + get; + set; + } + + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool PlayNoiseLoopInOutpostLevel + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.Yes), Editable] + public float WaterAmbienceVolume + { + get; + set; + } + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] public float WallEdgeExpandOutwardsAmount { @@ -556,8 +622,13 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } +#if CLIENT + public Sounds.Sound FlashSound { get; private set; } +#endif + #warning TODO: this should be in the unit test project (#3164) public static void CheckValidity() + { foreach (Biome biome in Biome.Prefabs) { @@ -575,7 +646,7 @@ namespace Barotrauma } } - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -590,14 +661,29 @@ namespace Barotrauma lp.Type == type && (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); - matchingLevelParams = biome.IsEmpty - ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) - : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + if (biomeId.IsEmpty) + { + //we don't want end levels when generating a completely random level (e.g. in mission mode) + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)); + } + else + { + bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome; + if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId))) + { + //in the end biome, we must choose level parameters meant specifically for the end levels + matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + else + { + matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId)); + } + } if (!matchingLevelParams.Any()) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); - if (!biome.IsEmpty) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biomeId.IsEmpty) { //try to find params that at least have a suitable type matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type); @@ -611,7 +697,7 @@ namespace Barotrauma if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty)) { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})"); } else { @@ -661,6 +747,11 @@ namespace Barotrauma case "waterparticles": WaterParticles = new Sprite(subElement); break; +#if CLIENT + case "flashsound": + FlashSound = GameMain.SoundManager.LoadSound(subElement); + break; +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index a5c1b3f85..c922a86c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -215,7 +215,7 @@ namespace Barotrauma PhysicsBody.FarseerBody.SetIsSensor(element.GetAttributeBool("sensor", true)); PhysicsBody.FarseerBody.BodyType = BodyType.Static; - ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } @@ -747,7 +747,7 @@ namespace Barotrauma Vector2 baseVel = GetWaterFlowVelocity(); if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero; - float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); + float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); float dist = Vector2.Distance(viewPosition, WorldPosition); if (dist > triggerSize) return Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 636c47c1a..58ae53c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -476,7 +476,7 @@ namespace Barotrauma bool leaveBehind = false; if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine)) { - System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit); + System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEitherExit); if (Submarine.MainSub.AtEndExit) { leaveBehind = sub.AtEndExit != Submarine.MainSub.AtEndExit; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7fa9b6fc2..6ae55544f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -3,7 +3,6 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -63,10 +62,17 @@ namespace Barotrauma public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; + public bool Visited => GameMain.GameSession?.Map?.IsVisited(this) ?? false; + public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; public int LocationTypeChangeCooldown; + /// + /// Is some mission blocking this location from changing its type? + /// + public bool LocationTypeChangesBlocked => availableMissions.Any(m => m.Prefab.BlockLocationTypeChanges); + public string BaseName { get => baseName; } public string Name { get; private set; } @@ -83,7 +89,11 @@ namespace Barotrauma public int PortraitId { get; private set; } - public Reputation Reputation { get; set; } + public Faction Faction { get; set; } + + public Faction SecondaryFaction { get; set; } + + public Reputation Reputation => Faction?.Reputation; public int TurnsInRadiation { get; set; } @@ -92,6 +102,7 @@ namespace Barotrauma public class StoreInfo { public Identifier Identifier { get; } + public Identifier MerchantFaction { get; private set; } public int Balance { get; set; } public List Stock { get; } = new List(); public List DailySpecials { get; } = new List(); @@ -101,6 +112,7 @@ namespace Barotrauma /// public int PriceModifier { get; set; } public Location Location { get; } + private float MaxReputationModifier => Location.StoreMaxReputationModifier; private StoreInfo(Location location) { @@ -125,6 +137,7 @@ namespace Barotrauma public StoreInfo(Location location, XElement storeElement) : this(location) { Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + MerchantFaction = storeElement.GetAttributeIdentifier(nameof(MerchantFaction), ""); Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute @@ -281,15 +294,17 @@ namespace Barotrauma { price = Location.DailySpecialPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(true); + // Adjust by current reputation + price *= GetReputationModifier(true); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) { - if (Location.Reputation?.Faction is { } faction && faction.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated) + var faction = GetMerchantOrLocationFactionIdentifier(); + if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive) { - price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false)); + price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag))); } price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false)); price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); @@ -312,8 +327,8 @@ namespace Barotrauma { price = Location.RequestGoodPriceModifier * price; } - // Adjust by current location reputation - price *= Location.GetStoreReputationModifier(false); + // Adjust by location reputation + price *= GetReputationModifier(false); var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (characters.Any()) @@ -326,6 +341,45 @@ namespace Barotrauma return Math.Max((int)price, 1); } + public void SetMerchantFaction(Identifier factionIdentifier) + { + MerchantFaction = factionIdentifier; + } + + public Identifier GetMerchantOrLocationFactionIdentifier() + { + return MerchantFaction.IfEmpty(Location.Faction?.Prefab.Identifier ?? Identifier.Empty); + } + + public float GetReputationModifier(bool buying) + { + var factionIdentifier = GetMerchantOrLocationFactionIdentifier(); + var reputation = GameMain.GameSession.Campaign.GetFaction(factionIdentifier)?.Reputation; + if (reputation == null) { return 1.0f; } + if (buying) + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + else + { + if (reputation.Value > 0.0f) + { + return MathHelper.Lerp(1.0f, 1.0f + MaxReputationModifier, reputation.Value / reputation.MaxReputation); + } + else + { + return MathHelper.Lerp(1.0f, 1.0f - MaxReputationModifier, reputation.Value / reputation.MinReputation); + } + } + } + public override string ToString() { return Identifier.Value; @@ -387,6 +441,8 @@ namespace Barotrauma } } + + public void SelectMission(Mission mission) { if (!SelectedMissions.Contains(mission) && mission != null) @@ -449,17 +505,22 @@ namespace Barotrauma public bool IsGateBetweenBiomes; - private struct LoadedMission + private readonly struct LoadedMission { - public MissionPrefab MissionPrefab { get; } - public int DestinationIndex { get; } - public bool SelectedMission { get; } + public readonly MissionPrefab MissionPrefab; + public readonly int TimesAttempted; + public readonly int OriginLocationIndex; + public readonly int DestinationIndex; + public readonly bool SelectedMission; - public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission) + public LoadedMission(XElement element) { - MissionPrefab = prefab; - DestinationIndex = destinationIndex; - SelectedMission = selectedMission; + var id = element.GetAttributeIdentifier("prefabid", Identifier.Empty); + MissionPrefab = MissionPrefab.Prefabs.TryGet(id, out var prefab) ? prefab : null; + TimesAttempted = element.GetAttributeInt("timesattempted", 0); + OriginLocationIndex = element.GetAttributeInt("origin", -1); + DestinationIndex = element.GetAttributeInt("destinationindex", -1); + SelectedMission = element.GetAttributeBool("selected", false); } } @@ -484,7 +545,7 @@ namespace Barotrauma /// /// Create a location from save data /// - public Location(XElement element) + public Location(CampaignMode campaign, XElement element) { Identifier locationTypeId = element.GetAttributeIdentifier("type", ""); bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type); @@ -497,12 +558,23 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); - IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); - MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); - TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); - StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); + IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); + MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); + + var factionIdentifier = element.GetAttributeIdentifier("faction", Identifier.Empty); + if (!factionIdentifier.IsEmpty) + { + Faction = campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + } + var secondaryFactionIdentifier = element.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + if (!secondaryFactionIdentifier.IsEmpty) + { + SecondaryFaction = campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + } Identifier biomeId = element.GetAttributeIdentifier("biome", Identifier.Empty); if (biomeId != Identifier.Empty) { @@ -640,13 +712,11 @@ namespace Barotrauma loadedMissions = new List(); foreach (XElement childElement in missionsElement.GetChildElements("mission")) { - var id = childElement.GetAttributeString("prefabid", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var destination = childElement.GetAttributeInt("destinationindex", -1); - var selected = childElement.GetAttributeBool("selected", false); - loadedMissions.Add(new LoadedMission(prefab, destination, selected)); + var loadedMission = new LoadedMission(childElement); + if (loadedMission.MissionPrefab != null) + { + loadedMissions.Add(loadedMission); + } } } } @@ -656,7 +726,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(LocationType newType, bool createStores = true) + public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true) { if (newType == Type) { return; } @@ -671,56 +741,61 @@ namespace Barotrauma Type = newType; Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); - if (Type.MissionIdentifiers.Any()) + if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced()); + if (Faction == null) + { + Faction = campaign.GetRandomFaction(Rand.RandSync.Unsynced); + } + if (SecondaryFaction == null) + { + SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.Unsynced); + } } - if (Type.MissionTags.Any()) + else { - UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); + Faction = null; + SecondaryFaction = null; } + UnlockInitialMissions(Rand.RandSync.Unsynced); + if (createStores) { CreateStores(force: true); } } - public void UnlockInitialMissions() + public void UnlockInitialMissions(Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(randSync)); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient)); + UnlockMissionByTag(Type.MissionTags.GetRandom(randSync)); } } public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab, connection); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab, connection)); } public void UnlockMission(MissionPrefab missionPrefab) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab); - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } + AddMission(InstantiateMission(missionPrefab)); } public Mission UnlockMissionByIdentifier(Identifier identifier) { if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) @@ -735,10 +810,7 @@ namespace Barotrauma { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); return mission; } return null; @@ -746,7 +818,8 @@ namespace Barotrauma public Mission UnlockMissionByTag(Identifier tag) { - var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return null; } + var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Contains(tag)); if (!matchingMissions.Any()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions found."); @@ -768,10 +841,7 @@ namespace Barotrauma { return null; } - availableMissions.Add(mission); -#if CLIENT - GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); -#endif + AddMission(mission); return mission; } else @@ -783,6 +853,20 @@ namespace Barotrauma return null; } + private void AddMission(Mission mission) + { + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#else + (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions); +#endif + } + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { if (prefab.IsAllowed(this, this)) @@ -877,6 +961,11 @@ namespace Barotrauma destination = Connections.First().OtherLocation(this); } var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); + if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count) + { + mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex]; + } + mission.TimesAttempted = loadedMission.TimesAttempted; availableMissions.Add(mission); if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } @@ -978,12 +1067,22 @@ namespace Barotrauma private string RandomName(LocationType type, Random rand, IEnumerable existingLocations) { + if (!type.ForceLocationName.IsNullOrEmpty()) + { + baseName = type.ForceLocationName.Value; + return baseName; + } baseName = type.GetRandomName(rand, existingLocations); if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; } nameFormatIndex = rand.Next() % type.NameFormats.Count; return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } + public void ForceName(string name) + { + baseName = Name = name; + } + public void LoadStores(XElement locationElement) { UpdateStoreIdentifiers(); @@ -1068,13 +1167,21 @@ namespace Barotrauma public int GetAdjustedMechanicalCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); - return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); + } + return (int)Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } public int GetAdjustedHealCost(int cost) { - float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + float discount = 0.0f; + if (Reputation != null) + { + discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + } return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); } @@ -1258,32 +1365,6 @@ namespace Barotrauma } } - public float GetStoreReputationModifier(bool buying) - { - if (buying) - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - else - { - if (Reputation.Value > 0.0f) - { - return MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation); - } - else - { - return MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation); - } - } - } - public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); @@ -1314,14 +1395,14 @@ namespace Barotrauma { return LevelData != null && LevelData.OutpostGenerationParamsExist && - LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType)); + LevelData.GetSuitableOutpostGenerationParams(this, LevelData).Any(p => p.CanHaveCampaignInteraction(interactionType)); } - public void Reset() + public void Reset(CampaignMode campaign) { if (Type != OriginalType) { - ChangeType(OriginalType); + ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; } CreateStores(force: true); @@ -1345,6 +1426,16 @@ namespace Barotrauma new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + + if (Faction != null) + { + locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier)); + } + if (SecondaryFaction != null) + { + locationElement.Add(new XAttribute("secondaryfaction", SecondaryFaction.Prefab.Identifier)); + } + LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -1403,6 +1494,7 @@ namespace Barotrauma { var storeElement = new XElement("store", new XAttribute("identifier", store.Identifier.Value), + new XAttribute(nameof(store.MerchantFaction), store.MerchantFaction), new XAttribute("balance", store.Balance), new XAttribute("pricemodifier", store.PriceModifier)); foreach (PurchasedItem item in store.Stock) @@ -1442,10 +1534,13 @@ namespace Barotrauma foreach (Mission mission in missions) { var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); - var i = map.Locations.IndexOf(location); + var destinationIndex = map.Locations.IndexOf(location); + var originIndex = map.Locations.IndexOf(mission.OriginLocation); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), - new XAttribute("destinationindex", i), + new XAttribute("destinationindex", destinationIndex), + new XAttribute(nameof(Mission.TimesAttempted), mission.TimesAttempted), + new XAttribute("origin", originIndex), new XAttribute("selected", selectedMissions.Contains(mission)))); } locationElement.Add(missionsElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 2a97aa299..f2211a472 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,8 +13,8 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private readonly List names; - private readonly List portraits = new List(); + private readonly ImmutableArray names; + private readonly ImmutableArray portraits; // private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; @@ -26,6 +26,8 @@ namespace Barotrauma public readonly LocalizedString Name; public readonly LocalizedString Description; + public readonly LocalizedString ForceLocationName; + public readonly float BeaconStationChance; public readonly CharacterTeamType OutpostTeam; @@ -39,7 +41,12 @@ namespace Barotrauma public bool IsEnterable { get; private set; } - public bool UseInMainMenu + /// + /// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone + /// + public bool AllowInRandomLevels { get; private set; } + + public bool UsePortraitInRandomLoadingScreens { get; private set; @@ -96,6 +103,8 @@ namespace Barotrauma public int DailySpecialsCount { get; } = 1; public int RequestedGoodsCount { get; } = 1; + public readonly bool ShowSonarMarker = true; + public override string ToString() { return $"LocationType (" + Identifier + ")"; @@ -108,9 +117,12 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); + UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true); + + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); @@ -126,23 +138,31 @@ namespace Barotrauma string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); - string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); - names = new List(); - foreach (string rawPath in rawNamePaths) + if (element.GetAttribute("name") != null) { - try - { - var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); - names.AddRange(File.ReadAllLines(path.Value).ToList()); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); - } + ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty)); } - if (!names.Any()) + else { - names.Add("ERROR: No names found"); + string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" }); + var names = new List(); + foreach (string rawPath in rawNamePaths) + { + try + { + var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim()); + names.AddRange(File.ReadAllLines(path.Value).ToList()); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e); + } + } + if (!names.Any()) + { + names.Add("ERROR: No names found"); + } + this.names = names.ToImmutableArray(); } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -172,7 +192,7 @@ namespace Barotrauma } MinCountPerZone[zoneIndex] = minCount; } - + var portraits = new List(); var hireableJobs = new List<(Identifier, float)>(); foreach (var subElement in element.Elements()) { @@ -213,6 +233,7 @@ namespace Barotrauma break; } } + this.portraits = portraits.ToImmutableArray(); this.hireableJobs = hireableJobs.ToImmutableArray(); } @@ -229,10 +250,10 @@ namespace Barotrauma return null; } - public Sprite GetPortrait(int portraitId) + public Sprite GetPortrait(int randomSeed) { - if (portraits.Count == 0) { return null; } - return portraits[Math.Abs(portraitId) % portraits.Count]; + if (portraits.Length == 0) { return null; } + return portraits[Math.Abs(randomSeed) % portraits.Length]; } public string GetRandomName(Random rand, IEnumerable existingLocations) @@ -245,17 +266,34 @@ namespace Barotrauma return unusedNames[rand.Next() % unusedNames.Count]; } } - return names[rand.Next() % names.Count]; + return names[rand.Next() % names.Length]; } - public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) + public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func predicate = null) { Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); LocationType[] allowedLocationTypes = - Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + Prefabs.Where(lt => + (predicate == null || predicate(lt)) && IsValid(lt)) .OrderBy(p => p.UintIdentifier).ToArray(); + bool IsValid(LocationType lt) + { + if (requireOutpost && !lt.HasOutpost) { return false; } + if (zone.HasValue) + { + if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; } + } + //if zone is not defined, this is a "random" (non-campaign) level + //-> don't choose location types that aren't allowed in those + else if (!lt.AllowInRandomLevels) + { + return false; + } + return true; + } + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 0639ceb6f..8d9e03673 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; -using Barotrauma.Extensions; +using static Barotrauma.LocationTypeChange; namespace Barotrauma { @@ -58,7 +58,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); - RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 0); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); @@ -91,37 +91,30 @@ namespace Barotrauma } } + public bool AnyWithinDistance(Location startLocation, int distance) + { + return Map.LocationOrConnectionWithinDistance( + startLocation, + maxDistance: distance, + criteria: MatchesLocation, + connectionCriteria: MatchesConnection); + } + public bool MatchesLocation(Location location) { return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); } - public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + public bool MatchesConnection(LocationConnection connection) { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && MatchesLocation(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) { - if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) - { - return true; - } - if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) - { - return true; - } - - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } - } + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; } - return false; } } @@ -141,24 +134,25 @@ namespace Barotrauma private readonly bool requireChangeMessages; private readonly string messageTag; - private ImmutableArray? messages = null; - public IReadOnlyList Messages - { - get - { - if (!messages.HasValue) - { - messages = TextManager.GetAll(messageTag).ToImmutableArray(); - if (messages.Value.None()) - { - if (requireChangeMessages) - { - DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); - } - } - } - return messages.Value; + public IReadOnlyList GetMessages(Faction faction) + { + if (faction != null && TextManager.ContainsTag(messageTag + "." + faction.Prefab.Identifier)) + { + return TextManager.GetAll(messageTag + "." + faction.Prefab.Identifier).ToImmutableArray(); + } + + if (TextManager.ContainsTag(messageTag)) + { + return TextManager.GetAll(messageTag).ToImmutableArray(); + } + else + { + if (requireChangeMessages) + { + DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); + } + return Enumerable.Empty().ToImmutableArray(); } } @@ -226,8 +220,9 @@ namespace Barotrauma if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } if (location.IsGateBetweenBiomes) { return 0.0f; } - if (DisallowedAdjacentLocations.Any() && - AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + if (DisallowedAdjacentLocations.Any() && + Map.LocationOrConnectionWithinDistance(location, DisallowedProximity, + (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) { return 0.0f; } @@ -246,7 +241,6 @@ namespace Barotrauma probability *= requirement.Probability; } } - if (location.ProximityTimer.ContainsKey(requirement)) { if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) @@ -265,25 +259,5 @@ namespace Barotrauma return probability; } - - private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) - { - if (currentDistance > maxDistance) { return false; } - if (currentDistance > 0 && predicate(location)) { return true; } - - checkedLocations ??= new HashSet(); - checkedLocations.Add(location); - - foreach (var connection in location.Connections) - { - var otherLocation = connection.OtherLocation(location); - if (!checkedLocations.Contains(otherLocation)) - { - if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } - } - } - - return false; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index df2c0679c..494653ba2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -39,7 +39,8 @@ namespace Barotrauma /// public readonly NamedEvent OnLocationChanged = new NamedEvent(); - public Location EndLocation { get; private set; } + private List endLocations = new List(); + public IReadOnlyList EndLocations { get { return endLocations; } } public Location StartLocation { get; private set; } @@ -69,13 +70,13 @@ namespace Barotrauma public List Locations { get; private set; } private readonly List locationsDiscovered = new List(); - private readonly List outpostsVisited = new List(); + private readonly List locationsVisited = new List(); public List Connections { get; private set; } public Radiation Radiation; - private bool wasLocationDiscoveryOrderTracked = true; + private bool trackedLocationDiscoveryAndVisitOrder = true; public Map(CampaignSettings settings) { @@ -117,7 +118,7 @@ namespace Barotrauma Locations.Add(null); } lairsFound |= subElement.GetAttributeString("type", "").Equals("lair", StringComparison.OrdinalIgnoreCase); - Locations[i] = new Location(subElement); + Locations[i] = new Location(campaign, subElement); break; case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement) @@ -127,11 +128,6 @@ namespace Barotrauma break; } } - System.Diagnostics.Debug.Assert(!Locations.Contains(null)); - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } List connectionElements = new List(); foreach (var subElement in element.Elements()) @@ -187,23 +183,72 @@ namespace Barotrauma } } } - int endLocationindex = element.GetAttributeInt("endlocation", -1); - if (endLocationindex >= 0 && endLocationindex < Locations.Count) + + if (element.GetAttribute("endlocation") != null) { - EndLocation = Locations[endLocationindex]; + //backwards compatibility + int endLocationIndex = element.GetAttributeInt("endlocation", -1); + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) + { + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); + } } else { - DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationindex}, location count: {Locations.Count})."); - foreach (Location location in Locations) + int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty()); + foreach (int endLocationIndex in endLocationindices) { - if (EndLocation == null || location.MapPosition.X > EndLocation.MapPosition.X) + if (endLocationIndex >= 0 && endLocationIndex < Locations.Count) { - EndLocation = location; + endLocations.Add(Locations[endLocationIndex]); + } + else + { + DebugConsole.AddWarning($"Error while loading the map. End location index out of bounds (index: {endLocationIndex}, location count: {Locations.Count})."); } } } + if (!endLocations.Any()) + { + DebugConsole.AddWarning($"Error while loading the map. No end location(s) found. Choosing the rightmost location as the end location..."); + Location endLocation = null; + foreach (Location location in Locations) + { + if (endLocation == null || location.MapPosition.X > endLocation.MapPosition.X) + { + endLocation = location; + } + } + endLocations.Add(endLocation); + } + + System.Diagnostics.Debug.Assert(endLocations.First().Biome != null, "End location biome was null."); + System.Diagnostics.Debug.Assert(endLocations.First().Biome.IsEndBiome, "The biome of the end location isn't the end biome."); + + //backwards compatibility (or support for loading maps created with mods that modify the end biome setup): + //if there's too few end locations, create more + int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count; + + Location firstEndLocation = EndLocations[0]; + for (int i = 0; i < missingOutpostCount; i++) + { + Vector2 mapPos = new Vector2( + MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)), + Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble())); + var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations) + { + Biome = endLocations.First().Biome + }; + newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f); + Locations.Add(newEndLocation); + endLocations.Add(newEndLocation); + } + //backwards compatibility: if the map contained the now-removed lairs and has no hunting grounds, create some hunting grounds if (lairsFound && !Connections.Any(c => c.LevelData.HasHuntingGrounds)) { @@ -214,6 +259,17 @@ namespace Barotrauma } } + foreach (var endLocation in EndLocations) + { + if (endLocation.Type?.ForceLocationName != null && + !endLocation.Type.ForceLocationName.IsNullOrEmpty()) + { + endLocation.ForceName(endLocation.Type.ForceLocationName.Value); + } + } + + AssignEndLocationLevelData(); + //backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml) float maxX = Locations.Select(l => l.MapPosition.X).Max(); if (maxX > Width) { Width = (int)(maxX + 10); } @@ -231,18 +287,13 @@ namespace Barotrauma Seed = seed; Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); - Generate(campaign.Settings); + Generate(campaign); if (Locations.Count == 0) { throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}"); } - for (int i = 0; i < Locations.Count; i++) - { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); - } - foreach (Location location in Locations) { if (location.Type.Identifier != "outpost") { continue; } @@ -263,6 +314,20 @@ namespace Barotrauma if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) { CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + StartLocation.SecondaryFaction = null; + var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost); + if (startOutpostFaction != null) + { + StartLocation.Faction = startOutpostFaction; + foreach (var connection in StartLocation.Connections) + { + var otherLocation = connection.OtherLocation(StartLocation); + if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + otherLocation.Faction = startOutpostFaction; + } + } + } } } @@ -273,7 +338,7 @@ namespace Barotrauma { if (StartLocation != null) { - StartLocation.LevelData = new LevelData(StartLocation, 0); + StartLocation.LevelData = new LevelData(StartLocation, this, 0); } //ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy @@ -289,7 +354,7 @@ namespace Barotrauma if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) { - CurrentLocation.ChangeType(tutorialOutpost); + CurrentLocation.ChangeType(campaign, tutorialOutpost); } Discover(CurrentLocation); Visit(CurrentLocation); @@ -307,7 +372,7 @@ namespace Barotrauma #region Generation - private void Generate(CampaignSettings settings) + private void Generate(CampaignMode campaign) { Connections.Clear(); Locations.Clear(); @@ -525,12 +590,14 @@ namespace Barotrauma connectionsBetweenZones[zone1].Add(connection); } } - else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1]) + else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] && + connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1]))) { connectionsBetweenZones[zone1].Add(connection); } } + var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList(); for (int i = Connections.Count - 1; i >= 0; i--) { int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X); @@ -538,9 +605,9 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; } - if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; } - - if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i])) + int leftZone = Math.Min(zone1, zone2); + if (generationParams.GateCount[leftZone] == 0) { continue; } + if (!connectionsBetweenZones[leftZone].Contains(Connections[i])) { Connections.RemoveAt(i); } @@ -552,11 +619,18 @@ namespace Barotrauma Connections[i].Locations[1]; if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), + leftMostLocation.ChangeType( + campaign, + LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"), createStores: false); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; + + if (leftMostLocation.Type.HasOutpost && campaign != null && gateFactions.Any()) + { + leftMostLocation.Faction = gateFactions[connectionsBetweenZones[leftZone].IndexOf(Connections[i]) % gateFactions.Count]; + } } } @@ -624,21 +698,27 @@ namespace Barotrauma } } - CreateEndLocation(); - foreach (Location location in Locations) { - location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome)); + location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome)); + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + location.SecondaryFaction ??= campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } location.CreateStores(force: true); } + foreach (LocationConnection connection in Connections) { connection.LevelData = new LevelData(connection); } + CreateEndLocation(campaign); + float CalculateDifficulty(float mapPosition, Biome biome) { - float settingsFactor = settings.LevelDifficultyMultiplier; + float settingsFactor = campaign.Settings.LevelDifficultyMultiplier; float minDifficulty = 0; float maxDifficulty = 100; float difficulty = mapPosition / Width * 100; @@ -707,18 +787,18 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null)); } - private void CreateEndLocation() + private void CreateEndLocation(CampaignMode campaign) { float zoneWidth = Width / generationParams.DifficultyZones; - Vector2 endPos = new Vector2(Width - zoneWidth / 2, Height / 2); + Vector2 endPos = new Vector2(Width - zoneWidth * 0.7f, Height / 2); float closestDist = float.MaxValue; - EndLocation = Locations.First(); + var endLocation = Locations.First(); foreach (Location location in Locations) { float dist = Vector2.DistanceSquared(endPos, location.MapPosition); if (location.Biome.IsEndBiome && dist < closestDist) { - EndLocation = location; + endLocation = location; closestDist = dist; } } @@ -732,17 +812,39 @@ namespace Barotrauma } } - if (EndLocation == null || previousToEndLocation == null) { return; } + if (endLocation == null || previousToEndLocation == null) { return; } + + endLocations = new List() { endLocation }; + if (endLocation.Biome.EndBiomeLocationCount > 1) + { + FindConnectedEndLocations(endLocation); + + void FindConnectedEndLocations(Location currLocation) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + foreach (var connection in currLocation.Connections) + { + if (connection.Biome != endLocation.Biome) { continue; } + var otherLocation = connection.OtherLocation(currLocation); + if (otherLocation != null && !endLocations.Contains(otherLocation)) + { + if (endLocations.Count >= endLocation.Biome.EndBiomeLocationCount) { return; } + endLocations.Add(otherLocation); + FindConnectedEndLocations(otherLocation); + } + } + } + } if (LocationType.Prefabs.TryGet("none", out LocationType locationType)) { - previousToEndLocation.ChangeType(locationType, createStores: false); + previousToEndLocation.ChangeType(campaign, locationType, createStores: false); } //remove all locations from the end biome except the end location for (int i = Locations.Count - 1; i >= 0; i--) { - if (Locations[i].Biome.IsEndBiome && Locations[i] != EndLocation) + if (Locations[i].Biome.IsEndBiome) { for (int j = Locations[i].Connections.Count - 1; j >= 0; j--) { @@ -753,7 +855,10 @@ namespace Barotrauma otherLocation?.Connections.Remove(connection); Connections.Remove(connection); } - Locations.RemoveAt(i); + if (!endLocations.Contains(Locations[i])) + { + Locations.RemoveAt(i); + } } } @@ -770,22 +875,39 @@ namespace Barotrauma } var newConnection = new LocationConnection(previousToEndLocation, connectTo) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + newConnection.LevelData = new LevelData(newConnection); Connections.Add(newConnection); previousToEndLocation.Connections.Add(newConnection); connectTo.Connections.Add(newConnection); } - var endConnection = new LocationConnection(previousToEndLocation, EndLocation) + var endConnection = new LocationConnection(previousToEndLocation, endLocation) { - Biome = EndLocation.Biome, + Biome = endLocation.Biome, Difficulty = 100.0f }; + endConnection.LevelData = new LevelData(endConnection); Connections.Add(endConnection); previousToEndLocation.Connections.Add(endConnection); - EndLocation.Connections.Add(endConnection); + endLocation.Connections.Add(endConnection); + + AssignEndLocationLevelData(); + } + + private void AssignEndLocationLevelData() + { + for (int i = 0; i < endLocations.Count; i++) + { + endLocations[i].LevelData.ReassignGenerationParams(Seed); + var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); + if (outpostParams != null) + { + endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams; + } + } } private void ExpandBiomes(List seeds) @@ -817,19 +939,48 @@ namespace Barotrauma public void MoveToNextLocation() { + if (SelectedLocation == null && Level.Loaded?.EndLocation != null) + { + //force the location at the end of the level to be selected, even if it's been deselect on the map + //(e.g. due to returning to an empty location the beginning of the level during the round) + SelectLocation(Level.Loaded.EndLocation); + } if (SelectedConnection == null) { - DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n"+Environment.StackTrace.CleanupStackTrace()); - return; + if (!endLocations.Contains(CurrentLocation)) + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } if (SelectedLocation == null) { - DebugConsole.ThrowError("Could not move to the next location (no location selected).\n" + Environment.StackTrace.CleanupStackTrace()); - return; + if (endLocations.Contains(CurrentLocation)) + { + int currentEndLocationIndex = endLocations.IndexOf(CurrentLocation); + if (currentEndLocationIndex < endLocations.Count - 1) + { + //more end locations to go, progress to the next one + SelectedLocation = endLocations[currentEndLocationIndex + 1]; + } + else + { + //at the last end location, end of campaign + SelectedLocation = StartLocation; + } + } + else + { + DebugConsole.ThrowError("Could not move to the next location (no connection selected).\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } } Location prevLocation = CurrentLocation; - SelectedConnection.Passed = true; + if (SelectedConnection != null) + { + SelectedConnection.Passed = true; + } CurrentLocation = SelectedLocation; Discover(CurrentLocation); @@ -898,12 +1049,24 @@ namespace Barotrauma Location prevSelected = SelectedLocation; SelectedLocation = Locations[index]; var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation(); - SelectedConnection = - Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? - Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + if (currentDisplayLocation == SelectedLocation) + { + SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } + else + { + SelectedConnection = + Connections.Find(c => c.Locations.Contains(currentDisplayLocation) && c.Locations.Contains(SelectedLocation)) ?? + Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation)); + } if (SelectedConnection?.Locked ?? false) { - DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace()); + string errorMsg = + $"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." + + $" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n" + + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError(errorMsg); } if (prevSelected != SelectedLocation) { @@ -979,7 +1142,7 @@ namespace Barotrauma } } - public void ProgressWorld(CampaignMode.TransitionType transitionType, float roundDuration) + public void ProgressWorld(CampaignMode campaign, CampaignMode.TransitionType transitionType, float roundDuration) { //one step per 10 minutes of play time int steps = (int)Math.Floor(roundDuration / (60.0f * 10.0f)); @@ -992,7 +1155,7 @@ namespace Barotrauma steps = Math.Min(steps, 5); for (int i = 0; i < steps; i++) { - ProgressWorld(); + ProgressWorld(campaign); } // always update specials every step @@ -1008,7 +1171,7 @@ namespace Barotrauma Radiation?.OnStep(steps); } - private void ProgressWorld() + private void ProgressWorld(CampaignMode campaign) { foreach (Location location in Locations) { @@ -1036,14 +1199,14 @@ namespace Barotrauma if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } - if (!ProgressLocationTypeChanges(location) && location.Discovered) + if (!ProgressLocationTypeChanges(campaign, location) && location.Discovered) { location.UpdateStores(); } } } - private bool ProgressLocationTypeChanges(Location location) + private bool ProgressLocationTypeChanges(CampaignMode campaign, Location location) { location.TimeSinceLastTypeChange++; location.LocationTypeChangeCooldown--; @@ -1063,7 +1226,7 @@ namespace Barotrauma location.PendingLocationTypeChange.Value.parentMission); if (location.PendingLocationTypeChange.Value.delay <= 0) { - return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange); } } } @@ -1096,7 +1259,7 @@ namespace Barotrauma } else { - return ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(campaign, location, selectedTypeChange); } return false; } @@ -1121,52 +1284,7 @@ namespace Barotrauma return false; } - public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) - { - if (startingLocation.Type.HasOutpost) - { - endingLocation = startingLocation; - return 0; - } - - int iterations = 0; - int distance = 0; - endingLocation = null; - - List testedLocations = new List(); - List locationsToTest = new List { startingLocation }; - - while (endingLocation == null && iterations < 100) - { - List nextTestingBatch = new List(); - for (int i = 0; i < locationsToTest.Count; i++) - { - Location testLocation = locationsToTest[i]; - for (int j = 0; j < testLocation.Connections.Count; j++) - { - Location potentialOutpost = testLocation.Connections[j].OtherLocation(testLocation); - if (potentialOutpost.Type.HasOutpost) - { - distance = iterations + 1; - endingLocation = potentialOutpost; - } - else if (!testedLocations.Contains(potentialOutpost)) - { - nextTestingBatch.Add(potentialOutpost); - } - } - - testedLocations.Add(testLocation); - } - - locationsToTest = nextTestingBatch; - iterations++; - } - - return distance; - } - - private bool ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { string prevName = location.Name; @@ -1176,12 +1294,14 @@ namespace Barotrauma return false; } + if (location.LocationTypeChangesBlocked) { return false; } + if (newType.OutpostTeam != location.Type.OutpostTeam || newType.HasOutpost != location.Type.HasOutpost) { location.ClearMissions(); } - location.ChangeType(newType); + location.ChangeType(campaign, newType); ChangeLocationTypeProjSpecific(location, prevName, change); foreach (var requirement in change.Requirements) { @@ -1193,6 +1313,50 @@ namespace Barotrauma return true; } + public static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + return GetDistanceToClosestLocationOrConnection(startLocation, maxDistance, criteria, connectionCriteria) <= maxDistance; + } + + /// + /// Get the shortest distance from the start location to another location that satisfies the specified criteria. + /// + /// The distance to a matching location, or int.MaxValue if none are found. + public static int GetDistanceToClosestLocationOrConnection(Location startLocation, int maxDistance, Func criteria, Func connectionCriteria = null) + { + int distance = 0; + var locationsToTest = new List() { startLocation }; + var nextBatchToTest = new HashSet(); + var checkedLocations = new HashSet(); + while (locationsToTest.Any()) + { + foreach (var location in locationsToTest) + { + checkedLocations.Add(location); + if (criteria(location)) { return distance; } + foreach (var connection in location.Connections) + { + if (connectionCriteria != null && connectionCriteria(connection)) + { + return distance; + } + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + nextBatchToTest.Add(otherLocation); + } + } + if (distance > maxDistance) { return int.MaxValue; } + } + distance++; + locationsToTest.Clear(); + locationsToTest.AddRange(nextBatchToTest); + nextBatchToTest.Clear(); + } + return int.MaxValue; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); partial void ClearAnimQueue(); @@ -1211,29 +1375,38 @@ namespace Barotrauma public void Visit(Location location) { if (location is null) { return; } - if (!location.HasOutpost()) { return; } - if (outpostsVisited.Contains(location)) { return; } - outpostsVisited.Add(location); + if (locationsVisited.Contains(location)) { return; } + locationsVisited.Add(location); + RemoveFogOfWarProjSpecific(location); } public void ClearLocationHistory() { locationsDiscovered.Clear(); - outpostsVisited.Clear(); + locationsVisited.Clear(); } public int? GetDiscoveryIndex(Location location) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } return locationsDiscovered.IndexOf(location); } - public int? GetVisitIndex(Location location) + public int? GetVisitIndex(Location location, bool includeLocationsWithoutOutpost = false) { - if (!wasLocationDiscoveryOrderTracked) { return null; } + if (!trackedLocationDiscoveryAndVisitOrder) { return null; } if (location is null) { return -1; } - return outpostsVisited.IndexOf(location); + int index = locationsVisited.IndexOf(location); + if (includeLocationsWithoutOutpost) { return index; } + int noOutpostLocations = 0; + for (int i = 0; i < index; i++) + { + if (locationsVisited[i] is not Location l) { continue; } + if (l.HasOutpost()) { continue; } + noOutpostLocations++; + } + return index - noOutpostLocations; } public bool IsDiscovered(Location location) @@ -1242,13 +1415,21 @@ namespace Barotrauma return locationsDiscovered.Contains(location); } + public bool IsVisited(Location location) + { + if (location is null) { return false; } + return locationsVisited.Contains(location); + } + + partial void RemoveFogOfWarProjSpecific(Location location); + /// /// Load a previously saved map from an xml element /// public static Map Load(CampaignMode campaign, XElement element) { Map map = new Map(campaign, element); - map.LoadState(element, false); + map.LoadState(campaign, element, false); #if CLIENT map.DrawOffset = -map.CurrentLocation.MapPosition; #endif @@ -1258,12 +1439,12 @@ namespace Barotrauma /// /// Load the state of an existing map from xml (current state of locations, where the crew is now, etc). /// - public void LoadState(XElement element, bool showNotifications) + public void LoadState(CampaignMode campaign, XElement element, bool showNotifications) { ClearAnimQueue(); SetLocation(element.GetAttributeInt("currentlocation", 0)); - if (!Version.TryParse(element.GetAttributeString("version", ""), out _)) + if (!Version.TryParse(element.GetAttributeString("version", ""), out Version version)) { DebugConsole.ThrowError("Incompatible map save file, loading the game failed."); return; @@ -1286,18 +1467,20 @@ namespace Barotrauma } location.LoadLocationTypeChange(subElement); - // Backwards compatibility + // Backwards compatibility: if the discovery status is defined in the location element, + // the game was saved using when the discovery order still wasn't being tracked if (subElement.GetAttributeBool("discovered", false)) { Discover(location); - wasLocationDiscoveryOrderTracked = false; + Visit(location); + trackedLocationDiscoveryAndVisitOrder = false; } Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); - location.ChangeType(newLocationType); + location.ChangeType(campaign, newLocationType); if (showNotifications && prevLocationType != location.Type) { var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); @@ -1308,6 +1491,12 @@ namespace Barotrauma } } + var factionIdentifier = subElement.GetAttributeIdentifier("faction", Identifier.Empty); + location.Faction = factionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == factionIdentifier); + + var secondaryFactionIdentifier = subElement.GetAttributeIdentifier("secondaryfaction", Identifier.Empty); + location.SecondaryFaction = secondaryFactionIdentifier.IsEmpty ? null : campaign.Factions.Find(f => f.Prefab.Identifier == secondaryFactionIdentifier); + location.LoadStores(subElement); location.LoadMissions(subElement); @@ -1321,32 +1510,45 @@ namespace Barotrauma Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; case "discovered": + bool trackedVisitedEmptyLocations = subElement.GetAttributeBool("trackedvisitedemptylocations", false); foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Discover(l); + if (GetLocation(childElement) is Location l) + { + Discover(l); + if (!trackedVisitedEmptyLocations) + { + if (!l.HasOutpost()) + { + Visit(l); + } + trackedLocationDiscoveryAndVisitOrder = false; + } + } } break; case "visited": foreach (var childElement in subElement.GetChildElements("location")) { - int index = childElement.GetAttributeInt("i", -1); - if (index < 0) { continue; } - if (Locations[index] is not Location l) { continue; } - Visit(l); + if (GetLocation(childElement) is Location l) + { + Visit(l); + } } break; } + + Location GetLocation(XElement element) + { + int index = element.GetAttributeInt("i", -1); + if (index < 0) { return null; } + return Locations[index]; + } } void Discover(Location location) { this.Discover(location, checkTalents: false); -#if CLIENT - RemoveFogOfWar(location); -#endif if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; @@ -1358,6 +1560,24 @@ namespace Barotrauma location?.InstantiateLoadedMissions(this); } + //backwards compatibility: + //if the save is from a version prior to the addition of faction-specific outposts, assign factions + if (version < new Version(1, 0) && Locations.None(l => l.Faction != null || l.SecondaryFaction != null)) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); + foreach (Location location in Locations) + { + if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC) + { + location.Faction = campaign.GetRandomFaction(Rand.RandSync.ServerAndClient); + if (location != StartLocation) + { + location.SecondaryFaction = campaign.GetRandomSecondaryFaction(Rand.RandSync.ServerAndClient); + } + } + } + } + int currentLocationConnection = element.GetAttributeInt("currentlocationconnection", -1); if (currentLocationConnection >= 0) { @@ -1396,7 +1616,7 @@ namespace Barotrauma mapElement.Add(new XAttribute("height", Height)); mapElement.Add(new XAttribute("selectedlocation", SelectedLocationIndex)); mapElement.Add(new XAttribute("startlocation", Locations.IndexOf(StartLocation))); - mapElement.Add(new XAttribute("endlocation", Locations.IndexOf(EndLocation))); + mapElement.Add(new XAttribute("endlocations", string.Join(',', EndLocations.Select(e => Locations.IndexOf(e))))); mapElement.Add(new XAttribute("seed", Seed)); for (int i = 0; i < Locations.Count; i++) @@ -1427,7 +1647,8 @@ namespace Barotrauma if (locationsDiscovered.Any()) { - var discoveryElement = new XElement("discovered"); + var discoveryElement = new XElement("discovered", + new XAttribute("trackedvisitedemptylocations", true)); foreach (Location location in locationsDiscovered) { int index = Locations.IndexOf(location); @@ -1437,10 +1658,10 @@ namespace Barotrauma mapElement.Add(discoveryElement); } - if (outpostsVisited.Any()) + if (locationsVisited.Any()) { var visitElement = new XElement("visited"); - foreach (Location location in outpostsVisited) + foreach (Location location in locationsVisited) { int index = Locations.IndexOf(location); var locationElement = new XElement("location", new XAttribute("i", index)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e74f92d4d..70e0ae742 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -61,7 +61,24 @@ namespace Barotrauma //the position and dimensions of the entity protected Rectangle rect; - public bool ExternalHighlight = false; + protected static readonly HashSet highlightedEntities = new HashSet(); + + public static IEnumerable HighlightedEntities => highlightedEntities; + + + private bool externalHighlight = false; + public bool ExternalHighlight + { + get { return externalHighlight; } + set + { + if (value != externalHighlight) + { + externalHighlight = value; + CheckIsHighlighted(); + } + } + } //is the mouse inside the rect private bool isHighlighted; @@ -69,7 +86,14 @@ namespace Barotrauma public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } - set { isHighlighted = value; } + set + { + if (value != IsHighlighted) + { + isHighlighted = value; + CheckIsHighlighted(); + } + } } public virtual Rectangle Rect @@ -160,7 +184,7 @@ namespace Barotrauma { if (!float.IsNaN(value)) { - _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); + _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999999f); if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } SpriteDepthOverrideIsSet = true; } @@ -364,6 +388,31 @@ namespace Barotrauma return true; } + protected virtual void CheckIsHighlighted() + { + if (IsHighlighted || ExternalHighlight) + { + highlightedEntities.Add(this); + } + else + { + highlightedEntities.Remove(this); + } + } + + private static readonly List tempHighlightedEntities = new List(); + public static void ClearHighlightedEntities() + { + highlightedEntities.RemoveWhere(e => e.Removed); + tempHighlightedEntities.Clear(); + tempHighlightedEntities.AddRange(highlightedEntities); + foreach (var entity in tempHighlightedEntities) + { + entity.IsHighlighted = false; + } + } + + public abstract MapEntity Clone(); public static List Clone(List entitiesToClone) @@ -675,6 +724,9 @@ namespace Barotrauma List entities = new List(); foreach (var element in parentElement.Elements()) { +#if CLIENT + GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested(); +#endif string typeName = element.Name.ToString(); Type t; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs index 7d9f33be6..feec0ea3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/BeaconStationInfo.cs @@ -17,6 +17,9 @@ namespace Barotrauma [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float MaxLevelDifficulty { get; set; } + [Serialize(Level.PlacementType.Bottom, IsPropertySaveable.Yes), Editable] + public Level.PlacementType Placement { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 965965805..8be567543 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -1,10 +1,6 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,23 +8,23 @@ namespace Barotrauma { public readonly static PrefabCollection Sets = new PrefabCollection(); - private readonly ImmutableArray Humans; - private bool Disposed { get; set; } - public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file, Identifier)).ToImmutableArray(); } - public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) + public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier, bool logError = true) { HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); if (prefab == null) { - DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + if (logError) + { + DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); + } return null; } return prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index f91f5e1a6..b6e506c30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -23,6 +23,15 @@ namespace Barotrauma get { return allowedLocationTypes; } } + + [Serialize(-1, IsPropertySaveable.Yes), Editable(MinValueInt = -1, MaxValueInt = 10)] + public int ForceToEndLocationIndex + { + get; + set; + } + + [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { @@ -30,6 +39,13 @@ namespace Barotrauma set; } + [Serialize(true, IsPropertySaveable.Yes, description: "Should the generator append generic (module flag \"none\") modules to the outpost to reach the total module count."), Editable] + public bool AppendToReachTotalModuleCount + { + get; + set; + } + [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { @@ -79,6 +95,13 @@ namespace Barotrauma set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool DrawBehindSubs + { + get; + set; + } + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { @@ -93,6 +116,12 @@ namespace Barotrauma set; } + public LevelData.LevelType? LevelType + { + get; + set; + } + [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } @@ -104,17 +133,21 @@ namespace Barotrauma public int Count; public int Order; + public Identifier RequiredFaction; + public ModuleCount(ContentXElement element) { Identifier = element.GetAttributeIdentifier("flag", element.GetAttributeIdentifier("moduletype", "")); Count = element.GetAttributeInt("count", 0); Order = element.GetAttributeInt("order", 0); + RequiredFaction = element.GetAttributeIdentifier("requiredfaction", Identifier.Empty); } public ModuleCount(Identifier id, int count) { Identifier = id; Count = count; + RequiredFaction = Identifier.Empty; } } @@ -132,16 +165,20 @@ namespace Barotrauma private readonly HumanPrefab humanPrefab = null; private readonly Identifier setIdentifier = Identifier.Empty; private readonly Identifier npcIdentifier = Identifier.Empty; + + public readonly Identifier FactionIdentifier = Identifier.Empty; - public Entry(HumanPrefab humanPrefab) + public Entry(HumanPrefab humanPrefab, Identifier factionIdentifier) { this.humanPrefab = humanPrefab; + this.FactionIdentifier = factionIdentifier; } - public Entry(Identifier setIdentifier, Identifier npcIdentifier) + public Entry(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) { this.setIdentifier = setIdentifier; this.npcIdentifier = npcIdentifier; + this.FactionIdentifier = factionIdentifier; } public HumanPrefab HumanPrefab @@ -150,12 +187,12 @@ namespace Barotrauma private readonly List entries = new List(); - public void Add(HumanPrefab humanPrefab) - => entries.Add(new Entry(humanPrefab)); + public void Add(HumanPrefab humanPrefab, Identifier factionIdentifier) + => entries.Add(new Entry(humanPrefab, factionIdentifier)); - public void Add(Identifier setIdentifier, Identifier npcIdentifier) - => entries.Add(new Entry(setIdentifier, npcIdentifier)); + public void Add(Identifier setIdentifier, Identifier npcIdentifier, Identifier factionIdentifier) + => entries.Add(new Entry(setIdentifier, npcIdentifier, factionIdentifier)); public IEnumerator GetEnumerator() { @@ -167,12 +204,23 @@ namespace Barotrauma IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerable GetByFaction(IEnumerable factions) + { + foreach (var entry in entries) + { + if (entry.FactionIdentifier == Identifier.Empty || factions.Any(f => f.Identifier == entry.FactionIdentifier)) + { + yield return entry.HumanPrefab; + } + } + } + public int Count => entries.Count; public HumanPrefab this[int index] => entries[index].HumanPrefab; } - private readonly ImmutableArray> humanPrefabCollections; + private readonly ImmutableArray humanPrefabCollections; public Dictionary SerializableProperties { get; private set; } @@ -184,9 +232,23 @@ namespace Barotrauma Name = element.GetAttributeString("name", Identifier.Value); allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + if (element.GetAttribute("leveltype") != null) + { + string levelTypeStr = element.GetAttributeString("leveltype", ""); + if (Enum.TryParse(levelTypeStr, out LevelData.LevelType parsedLevelType)) + { + LevelType = parsedLevelType; + } + else + { + DebugConsole.ThrowError($"Error in outpost generation parameters \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); + } + } + OutpostFilePath = element.GetAttributeContentPath(nameof(OutpostFilePath)); - var humanPrefabCollections = new List>(); + var humanPrefabCollections = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -199,14 +261,14 @@ namespace Barotrauma foreach (var npcElement in subElement.Elements()) { Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty); - + Identifier faction = npcElement.GetAttributeIdentifier("faction", Identifier.Empty); if (from != Identifier.Empty) { - newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty), faction); } else { - newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from)); + newCollection.Add(new HumanPrefab(npcElement, file, npcSetIdentifier: from), faction); } } humanPrefabCollections.Add(newCollection); @@ -254,10 +316,12 @@ namespace Barotrauma } } - public IReadOnlyList GetHumanPrefabs(Rand.RandSync randSync) + public IReadOnlyList GetHumanPrefabs(IEnumerable factions, Rand.RandSync randSync) { if (!humanPrefabCollections.Any()) { return Array.Empty(); } - return humanPrefabCollections.GetRandom(randSync); + + var collection = humanPrefabCollections.GetRandom(randSync); + return collection.GetByFaction(factions).ToImmutableList(); } public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 414c4c8da..6b95736f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -150,11 +150,14 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - List pendingModuleFlags = - onlyEntrance ? - (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : - SelectModules(outpostModules, generationParams); - + List pendingModuleFlags = new List(); + if (generationParams.ModuleCounts.Any()) + { + pendingModuleFlags = onlyEntrance ? + generationParams.ModuleCounts[0].Identifier.ToEnumerable().ToList() : + SelectModules(outpostModules, location, generationParams); + } + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -246,12 +249,17 @@ namespace Barotrauma wp.FindHull(); } } + EnableFactionSpecificEntities(sub, location); return sub; } remainingTries--; } +#if DEBUG + DebugConsole.ThrowError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#else DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); +#endif var outpostFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -268,6 +276,7 @@ namespace Barotrauma sub = new Submarine(prebuiltOutpostInfo); sub.Info.OutpostGenerationParams = generationParams; location?.RemoveTakenItems(); + EnableFactionSpecificEntities(sub, location); return sub; List loadEntities(Submarine sub) @@ -306,18 +315,27 @@ namespace Barotrauma hull.SetModuleTags(selectedModule.Info.OutpostModuleInfo.ModuleFlags); } - selectedModule.HullBounds = new Rectangle( - hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); - selectedModule.HullBounds = new Rectangle( - selectedModule.HullBounds.X, selectedModule.HullBounds.Y, - selectedModule.HullBounds.Width - selectedModule.HullBounds.X, selectedModule.HullBounds.Height - selectedModule.HullBounds.Y); - selectedModule.Bounds = new Rectangle( - wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height), - wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); - selectedModule.Bounds = new Rectangle( - selectedModule.Bounds.X, selectedModule.Bounds.Y, - selectedModule.Bounds.Width - selectedModule.Bounds.X, selectedModule.Bounds.Height - selectedModule.Bounds.Y); + if (!hullEntities.Any()) + { + selectedModule.HullBounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(hullEntities.Min(e => e.WorldRect.X), hullEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(hullEntities.Max(e => e.WorldRect.Right), hullEntities.Max(e => e.WorldRect.Y)); + selectedModule.HullBounds = new Rectangle(min, max - min); + } + + if (!wallEntities.Any()) + { + selectedModule.Bounds = new Rectangle(Point.Zero, Submarine.GridSize.ToPoint()); + } + else + { + Point min = new Point(wallEntities.Min(e => e.WorldRect.X), wallEntities.Min(e => e.WorldRect.Y - e.WorldRect.Height)); + Point max = new Point(wallEntities.Max(e => e.WorldRect.Right), wallEntities.Max(e => e.WorldRect.Y)); + selectedModule.Bounds = new Rectangle(min, max - min); + } if (selectedModule.PreviousModule != null) { @@ -406,6 +424,23 @@ namespace Barotrauma { LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps); } + if (generationParams.DrawBehindSubs) + { + foreach (var entity in allEntities) + { + if (entity is Structure structure) + { + //eww + structure.SpriteDepth = MathHelper.Lerp(0.999f, 0.9999f, structure.SpriteDepth); +#if CLIENT + foreach (var light in structure.Lights) + { + light.IsBackground = true; + } +#endif + } + } + } AlignLadders(selectedModules, entities); PowerUpOutpost(entities.SelectMany(e => e.Value)); if (generationParams.MaxWaterPercentage > 0.0f) @@ -436,7 +471,7 @@ namespace Barotrauma /// /// Select the number and types of the modules to use in the outpost /// - private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) + private static List SelectModules(IEnumerable modules, Location location, OutpostGenerationParams generationParams) { int totalModuleCount = generationParams.TotalModuleCount; var pendingModuleFlags = new List(); @@ -447,23 +482,29 @@ namespace Barotrauma while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { availableModulesFound = false; - foreach (var moduleFlag in generationParams.ModuleCounts) + foreach (var moduleCount in generationParams.ModuleCounts) { - if (pendingModuleFlags.Count(m => m == moduleFlag.Identifier) >= generationParams.GetModuleCount(moduleFlag.Identifier)) + if (!moduleCount.RequiredFaction.IsEmpty && + location.Faction?.Prefab.Identifier != moduleCount.RequiredFaction && + location.SecondaryFaction?.Prefab.Identifier != moduleCount.RequiredFaction) { continue; } - if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag.Identifier))) + if (pendingModuleFlags.Count(m => m == moduleCount.Identifier) >= generationParams.GetModuleCount(moduleCount.Identifier)) { - DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleFlag.Identifier}\" found)."); + continue; + } + if (!modules.Any(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier))) + { + DebugConsole.ThrowError($"Failed to add a module to the outpost (no modules with the flag \"{moduleCount.Identifier}\" found)."); continue; } availableModulesFound = true; - pendingModuleFlags.Add(moduleFlag.Identifier); + pendingModuleFlags.Add(moduleCount.Identifier); } } - pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f)).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); - while (pendingModuleFlags.Count < totalModuleCount) + pendingModuleFlags.OrderBy(f => generationParams.ModuleCounts.First(m => m.Identifier == f).Order).ThenBy(f => Rand.Value(Rand.RandSync.ServerAndClient)); + while (pendingModuleFlags.Count < totalModuleCount && generationParams.AppendToReachTotalModuleCount) { //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless @@ -514,12 +555,8 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) - { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - + if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; } + PlacedModule newModule = null; //try appending to the current module if possible if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) @@ -540,6 +577,7 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition otherGapPosition in GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) { + if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; } newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); if (newModule != null) { @@ -588,6 +626,16 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); } + + static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule) + { + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; } + } + return false; + } } /// @@ -1390,6 +1438,31 @@ namespace Barotrauma } } + private static void EnableFactionSpecificEntities(Submarine sub, Location location) + { + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (string.IsNullOrEmpty(me.Layer) || me.Submarine != sub) { continue; } + + var layerAsIdentifier = me.Layer.ToIdentifier(); + if (FactionPrefab.Prefabs.ContainsKey(layerAsIdentifier)) + { + me.HiddenInGame = + location?.Faction?.Prefab != FactionPrefab.Prefabs[layerAsIdentifier]; +#if CLIENT + //normally this is handled in LightComponent.OnMapLoaded, but this method is called after that + if (me.HiddenInGame && me is Item item) + { + foreach (var lightComponent in item.GetComponents()) + { + lightComponent.Light.Enabled = false; + } + } +#endif + } + } + } + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) @@ -1592,7 +1665,12 @@ namespace Barotrauma List killedCharacters = new List(); List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); - var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient); + + List factions = new List(); + if (location?.Faction != null) { factions.Add(location.Faction.Prefab); } + if (location?.SecondaryFaction != null) { factions.Add(location.SecondaryFaction.Prefab); } + + var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(factions, Rand.RandSync.ServerAndClient); foreach (HumanPrefab humanPrefab in humanPrefabs) { if (humanPrefab is null) { continue; } @@ -1611,7 +1689,7 @@ namespace Barotrauma for (int tries = 0; tries < 100; tries++) { var characterInfo = killedCharacter.CreateCharacterInfo(Rand.RandSync.ServerAndClient); - if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) + if (location != null && !location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { selectedCharacters.Add((killedCharacter, characterInfo)); break; @@ -1633,22 +1711,21 @@ namespace Barotrauma npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; npc.HumanPrefab = humanPrefab; - if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, humanPrefab.Identifier); + foreach (Identifier tag in humanPrefab.GetTags()) { - outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); + outpost.Info.AddOutpostNPCIdentifierOrTag(npc, tag); } - outpost.Info.OutpostNPCs[humanPrefab.Identifier].Add(npc); if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.KillableNPCs) { npc.CharacterHealth.Unkillable = true; } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); + humanPrefab.GiveItems(npc, outpost, gotoTarget as WayPoint, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; item.SpawnedInCurrentOutpost = true; } - npc.GiveIdCardTags(gotoTarget as WayPoint); humanPrefab.InitializeCharacter(npc, gotoTarget); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 532ce3957..696eda83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -34,6 +34,13 @@ namespace Barotrauma /// public const int DefaultAmount = 5; + private readonly Dictionary minReputation = new Dictionary(); + + /// + /// Minimum reputation needed to buy the item (Key = faction ID, Value = min rep) + /// + public IReadOnlyDictionary MinReputation => minReputation; + /// /// Support for the old style of determining item prices /// when there were individual Price elements for each location type @@ -70,6 +77,19 @@ namespace Barotrauma RequiresUnlock = requiresUnlock; } + private void LoadReputationRestrictions(XElement priceInfoElement) + { + foreach (XElement childElement in priceInfoElement.GetChildElements("reputation")) + { + Identifier factionId = childElement.GetAttributeIdentifier("faction", Identifier.Empty); + float rep = childElement.GetAttributeFloat("min", 0.0f); + if (!factionId.IsEmpty && rep > 0) + { + minReputation.Add(factionId, rep); + } + } + } + public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { var priceInfos = new List(); @@ -106,6 +126,7 @@ namespace Barotrauma displayNonEmpty: displayNonEmpty, requiresUnlock: requiresUnlock, storeIdentifier: storeIdentifier); + priceInfo.LoadReputationRestrictions(childElement); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); @@ -117,7 +138,8 @@ namespace Barotrauma minLevelDifficulty: minLevelDifficulty, buyingPriceMultiplier: buyingPriceMultiplier, displayNonEmpty: displayNonEmpty, - requiresUnlock: requiresUnlock); + requiresUnlock: requiresUnlock); + defaultPrice.LoadReputationRestrictions(element); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 18e48a4c2..97a7eec42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -175,7 +175,7 @@ namespace Barotrauma #endif foreach (var subElement in element.Elements()) { - switch (subElement.Name.ToString()) + switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": Sprite = new Sprite(subElement, lazyLoad: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index d5f4e7387..de5632bef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -213,9 +213,21 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.EndOutpost != null && DockedTo.Contains(Level.Loaded.EndOutpost)) + if (Level.Loaded.EndOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.EndOutpost)) + { + return true; + } + else if (Level.Loaded.EndOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.EndOutpost); + } + } + else if (Level.Loaded.Type == LevelData.LevelType.Outpost && Level.Loaded.StartOutpost != null) + { + //in outpost levels, the outpost is always the start outpost: check it if has an exit + return IsAtOutpostExit(Level.Loaded.StartOutpost); } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.EndExitPosition) < Level.ExitDistance * Level.ExitDistance); } @@ -226,14 +238,44 @@ namespace Barotrauma get { if (Level.Loaded == null) { return false; } - if (Level.Loaded.StartOutpost != null && DockedTo.Contains(Level.Loaded.StartOutpost)) + if (Level.Loaded.StartOutpost != null) { - return true; + if (DockedTo.Contains(Level.Loaded.StartOutpost)) + { + return true; + } + else if (Level.Loaded.StartOutpost.exitPoints.Any()) + { + return IsAtOutpostExit(Level.Loaded.StartOutpost); + } } return (Vector2.DistanceSquared(Position + HiddenSubPosition, Level.Loaded.StartExitPosition) < Level.ExitDistance * Level.ExitDistance); } } + public bool AtEitherExit => AtStartExit || AtEndExit; + + private bool IsAtOutpostExit(Submarine outpost) + { + if (outpost.exitPoints.Any()) + { + Rectangle worldBorders = Borders; + worldBorders.Location += WorldPosition.ToPoint(); + foreach (var exitPoint in outpost.exitPoints) + { + if (exitPoint.ExitPointSize != Point.Zero) + { + if (RectsOverlap(worldBorders, exitPoint.ExitPointWorldRect)) { return true; } + } + else + { + if (RectContains(worldBorders, exitPoint.WorldPosition)) { return true; } + } + } + } + return false; + } + public new Vector2 DrawPosition { @@ -284,6 +326,9 @@ namespace Barotrauma } } + private readonly List exitPoints = new List(); + public IReadOnlyList ExitPoints { get { return exitPoints; } } + public override string ToString() { return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")"; @@ -350,12 +395,23 @@ namespace Barotrauma } public WreckAI WreckAI { get; private set; } + public SubmarineTurretAI TurretAI { get; private set; } + public bool CreateWreckAI() { WreckAI = WreckAI.Create(this); return WreckAI != null; } + /// + /// Creates an AI that operates all the turrets on a sub, same as Thalamus but only operates the turrets. + /// + public bool CreateTurretAI() + { + TurretAI = new SubmarineTurretAI(this); + return TurretAI != null; + } + public void DisableWreckAI() { if (WreckAI == null) @@ -991,6 +1047,7 @@ namespace Barotrauma { WreckAI?.Update(deltaTime); } + TurretAI?.Update(deltaTime); if (subBody?.Body == null) { return; } @@ -1325,14 +1382,19 @@ namespace Barotrauma //place the sub above the top of the level HiddenSubPosition = HiddenSubStartPosition; - if (GameMain.GameSession != null && GameMain.GameSession.LevelData != null) + if (GameMain.GameSession?.LevelData != null) { HiddenSubPosition += Vector2.UnitY * GameMain.GameSession.LevelData.Size.Y; } - foreach (Submarine sub in loaded) + for (int i = 0; i < loaded.Count; i++) { - HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); + Submarine sub = loaded[i]; + HiddenSubPosition = + new Vector2( + //1st sub on the left side, 2nd on the right, etc + HiddenSubPosition.X * (i % 2 == 0 ? 1 : -1), + HiddenSubPosition.Y + sub.Borders.Height + 5000.0f); } IdOffset = IdRemap.DetermineNewOffset(); @@ -1460,10 +1522,15 @@ namespace Barotrauma MapEntity.MapLoaded(newEntities, true); foreach (MapEntity me in MapEntity.mapEntityList) { - if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + if (me.Submarine != this) { continue; } + if (me is LinkedSubmarine linkedSub) { linkedSub.LinkDummyToMainSubmarine(); } + else if (me is WayPoint wayPoint && wayPoint.SpawnType.HasFlag(SpawnType.ExitPoint)) + { + exitPoints.Add(wayPoint); + } } foreach (Hull hull in matchingHulls) @@ -1683,57 +1750,66 @@ namespace Barotrauma public static void Unload() { + if (Unloading) + { + DebugConsole.AddWarning($"Called {nameof(Submarine.Unload)} when already unloading."); + return; + } + Unloading = true; + try + { #if CLIENT - RoundSound.RemoveAllRoundSounds(); - GameMain.LightManager?.ClearLights(); + RoundSound.RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif - var _loaded = new List(loaded); - foreach (Submarine sub in _loaded) - { - sub.Remove(); - } - - loaded.Clear(); - - visibleEntities = null; - - if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } - - RemoveAll(); - - if (Item.ItemList.Count > 0) - { - List items = new List(Item.ItemList); - foreach (Item item in items) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { - DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); - try - { - item.Remove(); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); - } + sub.Remove(); } - Item.ItemList.Clear(); + + loaded.Clear(); + + visibleEntities = null; + + if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } + + RemoveAll(); + + if (Item.ItemList.Count > 0) + { + List items = new List(Item.ItemList); + foreach (Item item in items) + { + DebugConsole.ThrowError("Error while unloading submarines - item \"" + item.Name + "\" (ID:" + item.ID + ") not removed"); + try + { + item.Remove(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while removing \"" + item.Name + "\"!", e); + } + } + Item.ItemList.Clear(); + } + + Ragdoll.RemoveAll(); + PhysicsBody.RemoveAll(); + GameMain.World = null; + + Powered.Grids.Clear(); + + GC.Collect(); + + } + finally + { + Unloading = false; } - - Ragdoll.RemoveAll(); - - PhysicsBody.RemoveAll(); - - GameMain.World?.Clear(); - GameMain.World = null; - - Powered.Grids.Clear(); - - GC.Collect(); - - Unloading = false; } public override void Remove() @@ -1800,18 +1876,18 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed) { continue; } + if (connectedWp.IsObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition); var body = PickBody(start, end, null, Physics.CollisionLevel, allowInsideFixture: false); if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; break; } } @@ -1830,11 +1906,11 @@ namespace Barotrauma { if (node == null || node.Waypoint == null) { continue; } var wp = node.Waypoint; - if (wp.isObstructed) { continue; } + if (wp.IsObstructed) { continue; } foreach (var connection in node.connections) { var connectedWp = connection.Waypoint; - if (connectedWp.isObstructed || connectedWp.Ladders != null) { continue; } + if (connectedWp.IsObstructed || connectedWp.Ladders != null) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); @@ -1842,8 +1918,8 @@ namespace Barotrauma { if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { - connectedWp.isObstructed = true; - wp.isObstructed = true; + connectedWp.IsObstructed = true; + wp.IsObstructed = true; if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { nodes = new HashSet(); @@ -1865,7 +1941,7 @@ namespace Barotrauma { if (obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { - nodes.ForEach(n => n.Waypoint.isObstructed = false); + nodes.ForEach(n => n.Waypoint.IsObstructed = false); nodes.Clear(); obstructedNodes.Remove(otherSub); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 7867f1166..e498b3aa0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -18,6 +18,13 @@ namespace Barotrauma { public const float NeutralBallastPercentage = 0.07f; + public const Category CollidesWith = + Physics.CollisionItem | + Physics.CollisionLevel | + Physics.CollisionCharacter | + Physics.CollisionProjectile | + Physics.CollisionWall; + const float HorizontalDrag = 0.01f; const float VerticalDrag = 0.05f; const float MaxDrag = 0.1f; @@ -39,6 +46,7 @@ namespace Barotrauma } private float depthDamageTimer = 10.0f; + private float damageSoundTimer = 10.0f; private readonly Submarine submarine; @@ -146,9 +154,13 @@ namespace Barotrauma farseerBody.CollidesWith = collidesWith; farseerBody.Enabled = false; farseerBody.UserData = this; + if (sub.Info.IsOutpost) + { + farseerBody.BodyType = BodyType.Static; + } foreach (var mapEntity in MapEntity.mapEntityList) { - if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } + if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; } bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; @@ -185,13 +197,20 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine) { continue; } - if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } + + Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); + if (item.GetComponent() is Door door) + { + door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith); + door.OutsideSubmarineFixture.UserData = item; + } + + if (item.StaticBodyConfig == null) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale; float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale; - Vector2 simPos = ConvertUnits.ToSimUnits(item.Position); float simRadius = ConvertUnits.ToSimUnits(radius); float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); @@ -489,36 +508,58 @@ namespace Barotrauma if (Level.Loaded == null) { return; } //camera shake and sounds start playing 500 meters before crush depth - float depthEffectThreshold = 500.0f; - if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth - depthEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth - depthEffectThreshold) + const float CosmeticEffectThreshold = -500.0f; + //breaches won't get any more severe 500 meters below crush depth + const float MaxEffectThreshold = 500.0f; + const float MinWallDamageProbability = 0.1f; + const float MaxWallDamageProbability = 1.0f; + const float MinWallDamage = 50f; + const float MaxWallDamage = 500.0f; + const float MinCameraShake = 5f; + const float MaxCameraShake = 50.0f; + + if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold) { return; } - depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) { return; } - -#if CLIENT - SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); -#endif - - foreach (Structure wall in Structure.WallList) + damageSoundTimer -= deltaTime; + if (damageSoundTimer <= 0.0f) { - if (wall.Submarine != submarine) { continue; } - - float wallCrushDepth = wall.CrushDepth; - float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; - if (pastCrushDepth > 0) - { - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); - } - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) - { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Clamp(pastCrushDepth * 0.001f, 1.0f, 50.0f)); - } +#if CLIENT + SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f); +#endif + damageSoundTimer = Rand.Range(5.0f, 10.0f); } - depthDamageTimer = 10.0f; + depthDamageTimer -= deltaTime; + if (depthDamageTimer <= 0.0f) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.Submarine != submarine) { continue; } + + float wallCrushDepth = wall.CrushDepth; + float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; + float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f); + + if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; } + + float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio); + if (pastCrushDepth > 0) + { + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f); +#if CLIENT + SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f); +#endif + } + if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + { + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio)); + } + } + depthDamageTimer = Rand.Range(5.0f, 10.0f); + } } public void FlipX() @@ -623,7 +664,7 @@ namespace Barotrauma attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier; } - if (impactMass * attackMultiplier > MinImpactLimbMass) + if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static) { Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index a5c3ffac7..356ee4dcf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; #if DEBUG using System.IO; @@ -478,7 +479,6 @@ namespace Barotrauma hashTask = new Task(() => { hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); - Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -559,6 +559,14 @@ namespace Barotrauma } return structureCrushDepthsDefined; } + public void AddOutpostNPCIdentifierOrTag(Character npc, Identifier idOrTag) + { + if (!OutpostNPCs.ContainsKey(idOrTag)) + { + OutpostNPCs.Add(idOrTag, new List()); + } + OutpostNPCs[idOrTag].Add(npc); + } //saving/loading ---------------------------------------------------- public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) @@ -590,7 +598,6 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -752,6 +759,34 @@ namespace Barotrauma return doc; } + public int GetPrice(Location location = null, ImmutableHashSet characterList = null) + { + if (location is null) + { + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation is { } currentLocation) + { + location = currentLocation; + } + else + { + + return Price; + } + } + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + float price = Price; + + if (location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + + return (int)price; + } + public static int GetDefaultTier(int price) => price > 20000 ? HighestTier : price > 10000 ? 2 : 1; public const int HighestTier = 3; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 1a99f675a..2bb863cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -11,7 +11,7 @@ using Barotrauma.Extensions; namespace Barotrauma { [Flags] - public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8 }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 4, Corpse = 8, Submarine = 16, ExitPoint = 32 }; partial class WayPoint : MapEntity { @@ -29,7 +29,32 @@ namespace Barotrauma private HashSet tags; - public bool isObstructed; + public bool IsObstructed; + + public bool IsInWater => CurrentHull == null || CurrentHull.Surface > Position.Y; + + // Waypoints linked to doors are traversable, unless they are obstructed, because we filter them out in the setter of Gap.Open. + // The only way to add the open gaps should be by calling OnGapStateSchanged. + public bool IsTraversable => !IsObstructed && (openGaps == null || openGaps.Count == 0 || IsInWater); + + private HashSet openGaps; + /// + /// Only called by a Gap when the state changes. + /// So in practice used like an event callback, although technically just a method + /// (It would be cleaner to use an actual event in Gap.cs, but event registering and unregistering might cause an extra hassle) + /// + public void OnGapStateChanged(bool open, Gap gap) + { + openGaps ??= new HashSet(); + if (open) + { + openGaps.Add(gap); + } + else + { + openGaps.Remove(gap); + } + } private ushort gapId; public Gap ConnectedGap @@ -54,6 +79,12 @@ namespace Barotrauma set { spawnType = value; } } + public Point ExitPointSize { get; private set; } + + public Rectangle ExitPointWorldRect => new Rectangle( + (int)WorldPosition.X - ExitPointSize.X / 2, (int)WorldPosition.Y + ExitPointSize.Y / 2, + ExitPointSize.X, ExitPointSize.Y); + public Action OnLinksChanged { get; set; } public override string Name @@ -140,7 +171,9 @@ namespace Barotrauma { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) }, { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) }, - { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) } + { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) }, + { "Submarine", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) }, + { "ExitPoint", new Sprite("Content/UI/CommandUIBackground.png", new Rectangle(0,896,128,128)) } }; } #endif @@ -981,6 +1014,12 @@ namespace Barotrauma public override void OnMapLoaded() { + if (Submarine == null) + { + // Don't try to connect waypoints that are not linked to any submarines to hulls, stairs, gaps etc. + // Used to cause weird pathfinding errors on some outpost modules, because the waypoints of the main path or side path got linked to a hull in the outpost. + return; + } InitializeLinks(); FindHull(); FindStairs(); @@ -1018,7 +1057,6 @@ namespace Barotrauma int.Parse(element.GetAttribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); - Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); WayPoint w = new WayPoint(spawnType == SpawnType.Path ? Type.WayPoint : Type.SpawnPoint, rect, submarine, idRemap.GetOffsetId(element)) { @@ -1036,6 +1074,8 @@ namespace Barotrauma w.IdCardTags = idCardTagString.Split(','); } + w.ExitPointSize = element.GetAttributePoint("exitpointsize", Point.Zero); + w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); Identifier jobIdentifier = element.GetAttributeIdentifier("job", Identifier.Empty); @@ -1076,6 +1116,10 @@ namespace Barotrauma new XAttribute("x", (int)(rect.X - Submarine.HiddenSubPosition.X)), new XAttribute("y", (int)(rect.Y - Submarine.HiddenSubPosition.Y)), new XAttribute("spawn", spawnType)); + if (SpawnType == SpawnType.ExitPoint) + { + element.Add(new XAttribute("exitpointsize", XMLExtensions.PointToString(ExitPointSize))); + } if (!string.IsNullOrWhiteSpace(IdCardDesc)) element.Add(new XAttribute("idcarddesc", IdCardDesc)); if (idCardTags.Length > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index 29855637a..cba0112e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -348,12 +348,7 @@ namespace Barotrauma } private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : struct => - ReadOption(inc, attribute, bitField) switch - { - Some { Value: var value } => value, - None _ => null, - _ => throw new ArgumentOutOfRangeException() - }; + ReadOption(inc, attribute, bitField).TryUnwrap(out var value) ? value : null; private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : struct => WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); @@ -378,7 +373,7 @@ namespace Barotrauma { ToolBox.ThrowIfNull(option); - if (option.TryUnwrap(out T value)) + if (option.TryUnwrap(out T? value)) { bitField.WriteBoolean(true); if (TryFindBehavior(out ReadWriteBehavior behavior)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index d4b897c9c..0f33d9599 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking /// public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + order?.GetChatMessage(targetCharacter?.Name, (order.TargetEntity as Hull ?? sender?.CurrentHull)?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), targetCharacter, sender, isNewOrder) { @@ -110,7 +110,7 @@ namespace Barotrauma.Networking WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } - public struct OrderMessageInfo + public readonly struct OrderMessageInfo { public Identifier OrderIdentifier { get; } public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 668ba6362..c48514150 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -112,27 +112,24 @@ namespace Barotrauma.Networking switch (typeString) { case "float": - if (!(a is float?)) return false; - if (!(b is float?)) return false; - return MathUtils.NearlyEqual((float)a, (float)b); + if (a is not float fa) { return false; } + if (b is not float fb) { return false; } + return MathUtils.NearlyEqual(fa, fb); case "int": - if (!(a is int?)) return false; - if (!(b is int?)) return false; - return (int)a == (int)b; + if (a is not int ia) { return false; } + if (b is not int ib) { return false; } + return ia == ib; case "bool": - if (!(a is bool?)) return false; - if (!(b is bool?)) return false; - return (bool)a == (bool)b; + if (a is not bool ba) { return false; } + if (b is not bool bb) { return false; } + return ba == bb; case "Enum": - if (!(a is Enum)) return false; - if (!(b is Enum)) return false; - return ((Enum)a).Equals((Enum)b); + if (a is not Enum ea) { return false; } + if (b is not Enum eb) { return false; } + return ea.Equals(eb); default: - if (a == null || b == null) - { - return (a == null) == (b == null); - } - return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); + return ReferenceEquals(a,b) + || string.Equals(a?.ToString(), b?.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -205,7 +202,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) { overrideValue = Value; } + overrideValue ??= Value; switch (typeString) { case "float": @@ -295,10 +292,7 @@ namespace Barotrauma.Networking var saveProperties = SerializableProperty.GetProperties(this); foreach (var property in saveProperties) { - object value = property.GetValue(this); - if (value == null) { continue; } - - string typeName = SerializableProperty.GetSupportedTypeName(value.GetType()); + string typeName = SerializableProperty.GetSupportedTypeName(property.PropertyType); if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); @@ -758,6 +752,9 @@ namespace Barotrauma.Networking get; set; } + + [Serialize(defaultValue: "", IsPropertySaveable.Yes)] + public LanguageIdentifier Language { get; set; } private SelectionMode subSelectionMode; [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index 14ae70d17..72e237b6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System; using System.Collections.Generic; namespace Barotrauma diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 8286eee68..31ee2c9be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -119,7 +119,9 @@ namespace Barotrauma } private Shape bodyShape; - public float height, width, radius; + public float Height { get; private set; } + public float Width { get; private set; } + public float Radius { get; private set; } private readonly float density; @@ -385,7 +387,7 @@ namespace Barotrauma float height = ConvertUnits.ToSimUnits(colliderParams.Height) * colliderParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(colliderParams.Width) * colliderParams.Ragdoll.LimbScale; density = Physics.NeutralDensity; - CreateBody(width, height, radius, density, BodyType.Dynamic, + CreateBody(width, height, radius, density, colliderParams.BodyType, Physics.CollisionCharacter, Physics.CollisionWall | Physics.CollisionLevel, findNewContacts); @@ -434,9 +436,17 @@ namespace Barotrauma float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = Math.Max(forceDensity ?? element.GetAttributeFloat("density", Physics.NeutralDensity), MinDensity); Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - CreateBody(width, height, radius, density, bodyType, collisionCategory, collidesWith, findNewContacts); - _collisionCategories = collisionCategory; - _collidesWith = collidesWith; + if (element.GetAttributeBool("ignorecollision", false)) + { + _collisionCategories = Category.None; + _collidesWith = Category.None; + } + else + { + _collisionCategories = collisionCategory; + _collidesWith = collidesWith; + } + CreateBody(width, height, radius, density, bodyType, _collisionCategories, _collidesWith, findNewContacts); FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; @@ -472,9 +482,9 @@ namespace Barotrauma { DebugConsole.ThrowError("Invalid physics body dimensions (width: " + width + ", height: " + height + ", radius: " + radius + ")"); } - this.width = width; - this.height = height; - this.radius = radius; + Width = width; + Height = height; + Radius = radius; _collisionCategories = collisionCategory; _collidesWith = collidesWith; } @@ -492,16 +502,16 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - pos = new Vector2(0.0f, height / 2 + radius); + pos = new Vector2(0.0f, Height / 2 + Radius); break; case Shape.HorizontalCapsule: - pos = new Vector2(width / 2 + radius, 0.0f); + pos = new Vector2(Width / 2 + Radius, 0.0f); break; case Shape.Circle: - pos = new Vector2(0.0f, radius); + pos = new Vector2(0.0f, Radius); break; case Shape.Rectangle: - pos = height > width ? new Vector2(0, height / 2) : new Vector2(width / 2, 0); + pos = Height > Width ? new Vector2(0, Height / 2) : new Vector2(Width / 2, 0); break; default: throw new NotImplementedException(); @@ -514,13 +524,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return height / 2 + radius; + return Height / 2 + Radius; case Shape.HorizontalCapsule: - return width / 2 + radius; + return Width / 2 + Radius; case Shape.Circle: - return radius; + return Radius; case Shape.Rectangle: - return new Vector2(width * 0.5f, height * 0.5f).Length(); + return new Vector2(Width * 0.5f, Height * 0.5f).Length(); default: throw new NotImplementedException(); } @@ -531,13 +541,13 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - return new Vector2(radius * 2, height + radius * 2); + return new Vector2(Radius * 2, Height + Radius * 2); case Shape.HorizontalCapsule: - return new Vector2(width + radius * 2, radius * 2); + return new Vector2(Width + Radius * 2, Radius * 2); case Shape.Circle: - return new Vector2(radius * 2); + return new Vector2(Radius * 2); case Shape.Rectangle: - return new Vector2(width, height); + return new Vector2(Width, Height); default: throw new NotImplementedException(); } @@ -548,24 +558,24 @@ namespace Barotrauma switch (bodyShape) { case Shape.Capsule: - radius = Math.Max(size.X / 2, 0); - height = Math.Max(size.Y - size.X, 0); - width = 0; + Radius = Math.Max(size.X / 2, 0); + Height = Math.Max(size.Y - size.X, 0); + Width = 0; break; case Shape.HorizontalCapsule: - radius = Math.Max(size.Y / 2, 0); - width = Math.Max(size.X - size.Y, 0); - height = 0; + Radius = Math.Max(size.Y / 2, 0); + Width = Math.Max(size.X - size.Y, 0); + Height = 0; break; case Shape.Circle: - radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); - width = 0; - height = 0; + Radius = Math.Max(Math.Min(size.X, size.Y) / 2, 0); + Width = 0; + Height = 0; break; case Shape.Rectangle: - width = Math.Max(size.X, 0); - height = Math.Max(size.Y, 0); - radius = 0; + Width = Math.Max(size.X, 0); + Height = Math.Max(size.Y, 0); + Radius = 0; break; default: throw new NotImplementedException(); @@ -830,7 +840,7 @@ namespace Barotrauma Vector2 velDir = LinearVelocity / speed; float vel = speed * 2.0f; - float drag = vel * vel * Math.Max(height + radius * 2, height); + float drag = vel * vel * Math.Max(Height + Radius * 2, Height); dragForce = Math.Min(drag, Mass * 500.0f) * -velDir; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs index e5bf41bca..aa877db24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,6 +12,8 @@ namespace Barotrauma { public Identifier VariantOf { get; } + public T? ParentPrefab { get; set; } + public void InheritFrom(T parent); } @@ -20,8 +24,10 @@ namespace Barotrauma #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); } - - public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + + public delegate void VariantXMLChecker(XElement originalElement, XElement? variantElement, XElement result); + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement, VariantXMLChecker? checker = null) { XElement newElement = new XElement(variantElement.Name); newElement.Add(baseElement.Attributes()); @@ -31,6 +37,9 @@ namespace Barotrauma void ReplaceElement(XElement element, XElement replacement) { + XElement originalElement = new XElement(element); + + List newElementsFromBase = new List(element.Elements()); List elementsToRemove = new List(); foreach (XAttribute attribute in replacement.Attributes()) { @@ -48,6 +57,7 @@ namespace Barotrauma if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase)) { matchingElementFound = true; + newElementsFromBase.Clear(); elementsToRemove.AddRange(element.Elements()); break; } @@ -65,6 +75,7 @@ namespace Barotrauma ReplaceElement(subElement, replacementSubElement); } matchingElementFound = true; + newElementsFromBase.Remove(subElement); break; } i++; @@ -75,11 +86,16 @@ namespace Barotrauma } } elementsToRemove.ForEach(e => e.Remove()); + checker?.Invoke(originalElement, replacement, element); + foreach (XElement newElement in newElementsFromBase) + { + checker?.Invoke(newElement, null, newElement); + } } void ReplaceAttribute(XElement element, XAttribute newAttribute) { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + XAttribute? existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); if (existingAttribute == null) { element.Add(newAttribute); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 4b3b7edc9..e11ea36db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -125,7 +125,7 @@ namespace Barotrauma public Node? AddNodeAndInheritors(Identifier id) { - if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + if (!prefabCollection.TryGet(id, out T? _, requireInheritanceValid: false)) { return null; } if (!IdToNode.TryGetValue(id, out var node)) { @@ -139,24 +139,25 @@ namespace Barotrauma //all inheritors so let's just return this immediately return node; } - - prefabCollection - .Cast>() - .Where(p => p.VariantOf == id) - .Cast() - .ForEach(p => - { - var inheritorNode = AddNodeAndInheritors(p.Identifier); - if (inheritorNode is null) { return; } - RootNodes.Remove(inheritorNode); - inheritorNode.Parent = node; - node.Inheritors.Add(inheritorNode); - }); + var enumerator = prefabCollection.GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is not IImplementsVariants implementsVariants || implementsVariants.VariantOf != id) + { + continue; + } + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { continue; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + } return node; } - private void FindCycles(in Node node, HashSet uncheckedNodes) + private static void FindCycles(in Node node, HashSet uncheckedNodes) { HashSet checkedNodes = new HashSet(); List hierarchyPositions = new List(); @@ -183,24 +184,45 @@ namespace Barotrauma public void InvokeCallbacks() { HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); - IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + IdToNode.Values.ForEach(v => PrefabCollection.InheritanceTreeCollection.FindCycles(v, uncheckedNodes)); void invokeCallbacksForNode(Node node) { - if (!prefabCollection.TryGet(node.Identifier, out var p) || - !(p is IImplementsVariants prefab)) { return; } - if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + if (!prefabCollection.TryGet(node.Identifier, out var p, requireInheritanceValid: false) || + p is not IImplementsVariants prefab) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent, requireInheritanceValid: false)) + { + prefab.InheritFrom(parent); + prefab.ParentPrefab = parent; + } node.Inheritors.ForEach(invokeCallbacksForNode); } RootNodes.ForEach(invokeCallbacksForNode); } } + private static bool IsInheritanceValid(T? prefab) + { + if (prefab == null) { return false; } + return + prefab is not IImplementsVariants implementsVariants || + (implementsVariants.VariantOf.IsEmpty || (implementsVariants.ParentPrefab != null && IsInheritanceValid(implementsVariants.ParentPrefab))); + } + private void HandleInheritance(Identifier prefabIdentifier) => HandleInheritance(prefabIdentifier.ToEnumerable()); private void HandleInheritance(IEnumerable identifiers) { if (!implementsVariants) { return; } + foreach (var id in identifiers) + { + if (!TryGet(id, out T? prefab, requireInheritanceValid: false)) { continue; } + if (prefab is IImplementsVariants implementsVariants && !implementsVariants.VariantOf.IsEmpty) + { + //reset parent prefab, it'll get set in InvokeCallbacks if the inheritance is valid + implementsVariants.ParentPrefab = null; + } + } InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); inheritanceTreeCollection.AddNodesAndInheritors(identifiers); inheritanceTreeCollection.InvokeCallbacks(); @@ -213,9 +235,11 @@ namespace Barotrauma { get { - foreach (var prefab in prefabs) + foreach (var kvp in prefabs) { - yield return prefab; + var prefab = kvp.Value.ActivePrefab; + if (!IsInheritanceValid(prefab)) { continue; } + yield return kvp; } } } @@ -231,7 +255,8 @@ namespace Barotrauma { Prefab.DisallowCallFromConstructor(); var prefab = prefabs[identifier].ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + if (prefab != null && !IsPrefabOverriddenByFile(prefab) && + IsInheritanceValid(prefab)) { return prefab; } @@ -258,12 +283,17 @@ namespace Barotrauma /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) + { + return TryGet(identifier, out result, requireInheritanceValid: true); + } + + private bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result, bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector) && selector.ActivePrefab != null) { result = selector!.ActivePrefab; - return true; + return !requireInheritanceValid || IsInheritanceValid(result); } else { @@ -304,7 +334,7 @@ namespace Barotrauma public bool ContainsKey(Identifier identifier) { Prefab.DisallowCallFromConstructor(); - return prefabs.ContainsKey(identifier); + return TryGet(identifier, out _); } public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); @@ -460,6 +490,19 @@ namespace Barotrauma topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; OnSort?.Invoke(); HandleInheritance(this.Select(p => p.Identifier)); + + var enumerator = GetEnumerator(requireInheritanceValid: false); + while (enumerator.MoveNext()) + { + T p = enumerator.Current; + if (p is IImplementsVariants implementsVariants && !IsInheritanceValid(p)) + { + DebugConsole.ThrowError( + $"Error in content package \"{p.ContentFile.ContentPackage.Name}\": " + + $"could not find the prefab \"{implementsVariants.VariantOf}\" the prefab \"{p.Identifier}\" is configured as a variant of."); + continue; + } + } } /// @@ -467,15 +510,19 @@ namespace Barotrauma /// /// IEnumerator public IEnumerator GetEnumerator() + { + return GetEnumerator(requireInheritanceValid: true); + } + + private IEnumerator GetEnumerator(bool requireInheritanceValid) { Prefab.DisallowCallFromConstructor(); - foreach (var kpv in prefabs) + foreach (var kvp in prefabs) { - var prefab = kpv.Value.ActivePrefab; - if (prefab != null && !IsPrefabOverriddenByFile(prefab)) - { - yield return prefab; - } + var prefab = kvp.Value.ActivePrefab; + if (prefab == null || IsPrefabOverriddenByFile(prefab)) { continue; } + if (requireInheritanceValid && !IsInheritanceValid(prefab)) { continue; } + yield return prefab; } } @@ -485,7 +532,7 @@ namespace Barotrauma /// IEnumerator IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + return GetEnumerator(requireInheritanceValid: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 62b8c8056..557860b69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -201,17 +201,28 @@ namespace Voronoi2 public bool IsPointInside(Vector2 point) { + if (!IsPointInsideAABB(point, margin: 0.0f)) { return false; } Vector2 transformedPoint = point - Translation; - if (Edges.All(e => e.Point1.X < transformedPoint.X && e.Point2.X < transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y < transformedPoint.Y && e.Point2.Y < transformedPoint.Y)) { return false; } - if (Edges.All(e => e.Point1.X > transformedPoint.X && e.Point2.X > transformedPoint.X)) { return false; } - if (Edges.All(e => e.Point1.Y > transformedPoint.Y && e.Point2.Y > transformedPoint.Y)) { return false; } foreach (GraphEdge edge in Edges) { if (MathUtils.LinesIntersect(transformedPoint, Center - Translation, edge.Point1, edge.Point2)) { return false; } } return true; } + + public bool IsPointInsideAABB(Vector2 point2, float margin) + { + Vector2 transformedPoint = point2 - Translation; + Vector2 max = transformedPoint + Vector2.One * margin; + Vector2 min = transformedPoint - Vector2.One * margin; + + if (Edges.All(e => e.Point1.X < min.X && e.Point2.X < min.X)) { return false; } + if (Edges.All(e => e.Point1.Y < min.Y && e.Point2.Y < min.Y)) { return false; } + if (Edges.All(e => e.Point1.X > max.X && e.Point2.X > max.X)) { return false; } + if (Edges.All(e => e.Point1.Y > max.Y && e.Point2.Y > max.Y)) { return false; } + + return true; + } } public class GraphEdge diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 53d5551b8..de9e5264f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -61,10 +61,7 @@ namespace Barotrauma GameMain.GameSession?.CrewManager?.AutoShowCrewList(); #endif - foreach (MapEntity entity in MapEntity.mapEntityList) - { - entity.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if RUN_PHYSICS_IN_SEPARATE_THREAD var physicsThread = new Thread(ExecutePhysics) @@ -139,10 +136,7 @@ namespace Barotrauma { if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } } - foreach (MapEntity e in MapEntity.mapEntityList) - { - e.IsHighlighted = false; - } + MapEntity.ClearHighlightedEntities(); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index c78758bf3..5e5317022 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -154,6 +154,7 @@ namespace Barotrauma { typeof(float), "float" }, { typeof(string), "string" }, { typeof(Identifier), "identifier" }, + { typeof(LanguageIdentifier), "languageidentifier" }, { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, @@ -240,7 +241,7 @@ namespace Barotrauma switch (typeName) { case "bool": - bool boolValue = value == "true" || value == "True"; + bool boolValue = value.ToIdentifier() == "true"; if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; @@ -290,6 +291,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, value.ToIdentifier()); break; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, value.ToLanguageIdentifier()); + break; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString(value)); break; @@ -373,6 +377,9 @@ namespace Barotrauma case "identifier": PropertyInfo.SetValue(parentObject, new Identifier((string)value)); return true; + case "languageidentifier": + PropertyInfo.SetValue(parentObject, ((string)value).ToLanguageIdentifier()); + return true; case "localizedstring": PropertyInfo.SetValue(parentObject, new RawLString((string)value)); return true; @@ -556,7 +563,7 @@ namespace Barotrauma public static string GetSupportedTypeName(Type type) { - if (type.IsEnum) return "Enum"; + if (type.IsEnum) { return "Enum"; } if (!supportedTypes.TryGetValue(type, out string typeName)) { return null; @@ -693,6 +700,29 @@ namespace Barotrauma case nameof(Character.SpeedMultiplier): { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } break; + case nameof(Character.PropulsionSpeedMultiplier): + { if (parentObject is Character character) { value = character.PropulsionSpeedMultiplier; return true; } } + break; + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { value = character.LowPassMultiplier; return true; } } + break; + case nameof(Character.HullOxygenPercentage): + { + if (parentObject is Character character) + { + value = character.HullOxygenPercentage; + return true; + } + else if (parentObject is Item item) + { + value = item.HullOxygenPercentage; + return true; + } + } + break; + case nameof(Door.Stuck): + { if (parentObject is Door door) { value = door.Stuck; return true; } } + break; } return false; } @@ -740,6 +770,23 @@ namespace Barotrauma case nameof(Controller.State): if (parentObject is Controller controller) { value = controller.State; return true; } break; + case nameof(Character.InWater): + { + if (parentObject is Character character) + { + value = character.InWater; + return true; + } + else if (parentObject is Item item) + { + value = item.InWater; + return true; + } + } + break; + case nameof(Rope.Snapped): + if (parentObject is Rope rope) { value = rope.Snapped; return true; } + break; } return false; } @@ -769,7 +816,7 @@ namespace Barotrauma switch (Name) { case nameof(Item.Condition): - if (parentObject is Item item) { item.Condition = value; return true; } + { if (parentObject is Item item) { item.Condition = value; return true; } } break; case nameof(Powered.Voltage): if (parentObject is Powered powered) { powered.Voltage = value; return true; } @@ -801,6 +848,9 @@ namespace Barotrauma case nameof(Character.PropulsionSpeedMultiplier): { if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } } break; + case nameof(Item.Scale): + { if (parentObject is Item item) { item.Scale = value; return true; } } + break; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 9e662479a..fa9610d09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -484,9 +484,18 @@ namespace Barotrauma { var attr = element?.GetAttribute(name); if (attr == null) { return defaultValue; } - return Enum.TryParse(attr.Value, true, out T result) ? result : - int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt) ? Unsafe.As(ref resultInt) : - defaultValue; + + if (Enum.TryParse(attr.Value, true, out T result)) + { + return result; + } + else if (int.TryParse(attr.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt)) + { + return Unsafe.As(ref resultInt); + } + DebugConsole.ThrowError($"Error in {attr}! \"{attr}\" is not a valid {typeof(T).Name} value"); + return default; + } public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) @@ -608,10 +617,18 @@ namespace Barotrauma return mouseButton; } else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && - (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + Enum.GetValues().Contains((MouseButton)mouseButtonInt)) { return (MouseButton)mouseButtonInt; } + else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse; + } + else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase)) + { + return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse; + } return defaultValue; } #endif @@ -807,7 +824,15 @@ namespace Barotrauma #endif return Color.White; } - + if (stringColor.StartsWith("faction.", StringComparison.OrdinalIgnoreCase)) + { + Identifier factionId = stringColor.Substring(8).ToIdentifier(); + if (FactionPrefab.Prefabs.TryGet(factionId, out var faction)) + { + return faction.IconColor; + } + return Color.White; + } string[] strComponents = stringColor.Split(','); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs index 5886c8ba3..617058963 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -1,13 +1,134 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.IO; +using XmlWriterSettings = System.Xml.XmlWriterSettings; +#nullable enable namespace Barotrauma { - public class CreatureMetrics + public static class CreatureMetrics { - public readonly HashSet RecentlyEncountered = new HashSet(); - public readonly HashSet Encountered = new HashSet(); - public readonly HashSet Killed = new HashSet(); + private const string path = "creature_metrics.xml"; - public readonly static CreatureMetrics Instance = new CreatureMetrics(); + /// + /// Resets every round. + /// + public static HashSet RecentlyEncountered { get; private set; } = new HashSet(); + public static HashSet Encountered { get; private set; } = new HashSet(); + public static HashSet Unlocked { get; private set; } = new HashSet(); + public static HashSet Killed { get; private set; } = new HashSet(); + public static bool IsInitialized { get; private set; } + public static bool UnlockAll { get; set; } + + public static void Init() + { + IsInitialized = true; + if (File.Exists(path)) + { + Load(); + } + Save(); + } + + private static void Load() + { + XDocument doc = XMLExtensions.TryLoadXml(path); + XElement? root = doc?.Root; + if (root == null) + { + DebugConsole.AddWarning($"Failed to load creature metrics from {path}!"); + return; + } + UnlockAll = root.GetAttributeBool(nameof(UnlockAll), UnlockAll); + Unlocked = new HashSet(root.GetAttributeIdentifierArray(nameof(Unlocked), Array.Empty())); + Encountered = new HashSet(root.GetAttributeIdentifierArray(nameof(Encountered), Array.Empty())); + Killed = new HashSet(root.GetAttributeIdentifierArray(nameof(Killed), Array.Empty())); + SyncSets(); + } + + public static void Save() + { + if (!IsInitialized) + { + throw new Exception("Creature Metrics not yet initialized!"); + } + SyncSets(); + XDocument configDoc = new XDocument(); + XElement root = new XElement("CreatureMetrics"); + configDoc.Add(root); + root.SetAttributeValue(nameof(UnlockAll), UnlockAll); + root.SetAttributeValue(nameof(Unlocked), string.Join(",", Unlocked).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Encountered), string.Join(",", Encountered).Trim().ToLowerInvariant()); + root.SetAttributeValue(nameof(Killed), string.Join(",", Killed).Trim().ToLowerInvariant()); + configDoc.SaveSafe(path); + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + try + { + using var writer = XmlWriter.Create(path, settings); + configDoc.WriteTo(writer); + writer.Flush(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving creature metrics failed.", e); + GameAnalyticsManager.AddErrorEventOnce("CreatureMetrics.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving creature metrics failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + + public static void RecordKill(Identifier species) + { + AddEncounter(species); + if (!Killed.Contains(species)) + { + Killed.Add(species); + } + } + + public static void AddEncounter(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Encountered.Contains(species)) { return; } + Encountered.Add(species); + RecentlyEncountered.Add(species); + UnlockInEditor(species); + } + + private static IEnumerable? vanillaCharacters; + public static void UnlockInEditor(Identifier species) + { + if (species == CharacterPrefab.HumanSpeciesName) { return; } + if (Unlocked.Contains(species)) { return; } + vanillaCharacters ??= GameMain.VanillaContent.GetFiles(); + var contentFile = CharacterPrefab.FindBySpeciesName(species); + if (contentFile == null) { return; } + if (!vanillaCharacters.Contains(contentFile.ContentFile)) + { + // Don't try to unlock custom characters. They are always unlocked. + return; + } + Unlocked.Add(species); + } + + private static void SyncSets() + { + // Ensure that all killed are also encountered and both unlocked. + // Otherwise we could permanently hide some creatures by manually adding them to the encountered or by removing from unlocked in the xml file. + foreach (var species in Killed) + { + Encountered.Add(species); + } + foreach (var species in Encountered) + { + Unlocked.Add(species); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 5776bc9e0..2c8f0353b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -110,6 +110,7 @@ namespace Barotrauma #if CLIENT retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); + retVal.SavedCampaignSettings = element.GetChildElement("campaignsettings"); LoadSubEditorImages(element); #endif @@ -139,6 +140,9 @@ namespace Barotrauma public bool DisableInGameHints; public bool EnableSubmarineAutoSave; public Identifier QuickStartSub; +#if CLIENT + public XElement SavedCampaignSettings; +#endif #if DEBUG public bool UseSteamMatchmaking; public bool RequireSteamAuthentication; @@ -230,7 +234,7 @@ namespace Barotrauma SoundVolume = 0.5f, UiVolume = 0.3f, VoiceChatVolume = 0.5f, - VoiceChatCutoffPrevention = 0, + VoiceChatCutoffPrevention = 200, MicrophoneVolume = 5, MuteOnFocusLost = false, DynamicRangeCompressionEnabled = true, @@ -618,6 +622,8 @@ namespace Barotrauma root.Add(inventoryKeyMappingElement); SubEditorScreen.ImageManager.Save(root); + + root.Add(CampaignSettings.CurrentSettings.Save()); #endif configDoc.SaveSafe(PlayerConfigPath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs new file mode 100644 index 000000000..55c5892e3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/ServerLanguageOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma; + +static class ServerLanguageOptions +{ + public readonly record struct LanguageOption( + string Label, + LanguageIdentifier Identifier, + ImmutableArray MapsFrom) + { + public static LanguageOption FromXElement(XElement element) + => new LanguageOption( + Label: + element.GetAttributeString("label", ""), + Identifier: + element.GetAttributeIdentifier("identifier", LanguageIdentifier.None.Value) + .ToLanguageIdentifier(), + MapsFrom: + element.GetAttributeIdentifierArray("mapsFrom", Array.Empty()) + .Select(id => id.ToLanguageIdentifier()).ToImmutableArray()); + } + + public static readonly ImmutableArray Options; + + static ServerLanguageOptions() + { + var languageOptionElements + = XMLExtensions.TryLoadXml("Data/languageoptions.xml")?.Root?.Elements() + ?? Enumerable.Empty(); + Options = languageOptionElements + // Convert the XElements into LanguageOptions immediately since they can be worked with more directly + .Select(LanguageOption.FromXElement) + // Remove options with duplicate identifiers + .DistinctBy(p => p.Identifier) + // Remove options where the label is empty or the identifier is missing + .Where(p => !p.Label.IsNullOrWhiteSpace() && p.Identifier != LanguageIdentifier.None) + // Sort the options based on the lexicographical order of the labels + .OrderBy(p => p.Label) + .ToImmutableArray(); + } + + public static LanguageIdentifier PickLanguage(LanguageIdentifier id) + { + if (id == LanguageIdentifier.None) + { + id = GameSettings.CurrentConfig.Language; + } + + foreach (var (_, identifier, mapsFrom) in Options) + { + if (id == identifier || mapsFrom.Contains(id)) + { + return identifier; + } + } + + return TextManager.DefaultLanguage; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b241816f3..514e207b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -268,7 +268,7 @@ namespace Barotrauma { if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == targetCharacter.Params.CompareGroup(AttributeValue.ToIdentifier()); + return (Operator == OperatorType.Equals) == CharacterParams.CompareGroup(AttributeValue.ToIdentifier(), targetCharacter.Group); } case ConditionType.EntityType: switch (AttributeValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 7c6b305ba..9faabef05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -208,16 +208,8 @@ namespace Barotrauma AimSpreadRad = MathHelper.ToRadians(element.GetAttributeFloat("aimspread", 0f)); Equip = element.GetAttributeBool("equip", false); - string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); - if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); - } - string rotationTypeStr = element.GetAttributeString("rotationtype", RotationRad != 0 ? "Fixed" : "Target"); - if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) - { - DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); - } + SpawnPosition = element.GetAttributeEnum("spawnposition", SpawnPositionType.This); + RotationType = element.GetAttributeEnum("rotationtype", RotationRad != 0 ? SpawnRotationType.Fixed : SpawnRotationType.Target); } } @@ -347,6 +339,8 @@ namespace Barotrauma public Dictionary intervalTimers = new Dictionary(); + private readonly bool oneShot; + public static readonly List DurationList = new List(); /// @@ -389,7 +383,8 @@ namespace Barotrauma private readonly List triggeredEvents; private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), - triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); + triggeredEventEntityTag = "statuseffectentity".ToIdentifier(), + triggeredEventUserTag = "statuseffectuser".ToIdentifier(); private Character user; @@ -468,6 +463,8 @@ namespace Barotrauma } } + public bool Disabled { get; private set; } + public static StatusEffect Load(ContentXElement element, string parentDebugName) { if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null) @@ -624,6 +621,9 @@ namespace Barotrauma propertyAttributes.Add(attribute); } break; + case "oneshot": + oneShot = attribute.GetAttributeBool(false); + break; default: propertyAttributes.Add(attribute); break; @@ -1153,6 +1153,7 @@ namespace Barotrauma public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type || !HasRequiredItems(entity)) { return; } if (!IsValidTarget(target)) { return; } @@ -1177,6 +1178,7 @@ namespace Barotrauma protected readonly List currentTargets = new List(); public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (this.type != type) { return; } if (ShouldWaitForInterval(entity, deltaTime)) { return; } @@ -1276,6 +1278,7 @@ namespace Barotrauma protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (lifeTime > 0) { lifeTimer -= deltaTime; @@ -1472,7 +1475,6 @@ namespace Barotrauma if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { - if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction newAffliction = affliction; if (target is Character character) { @@ -1666,18 +1668,20 @@ namespace Barotrauma { if (!triggeredEventTargetTag.IsEmpty) { - List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - - if (eventTargets.Count > 0) + IEnumerable eventTargets = targets.Where(t => t is Entity); + if (eventTargets.Any()) { - scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); + scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets.Cast().ToList()); } } - if (!triggeredEventEntityTag.IsEmpty && entity != null) { scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } + if (!triggeredEventUserTag.IsEmpty && user != null) + { + scriptedEvent.Targets.Add(triggeredEventUserTag, new List { user }); + } } } } @@ -1776,7 +1780,10 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandomUnsynced()); + if (spawnItems.Count > 0) + { + SpawnItem(spawnItems.GetRandomUnsynced()); + } } else { @@ -1917,9 +1924,15 @@ namespace Barotrauma } else if (entity is Item item) { - var itemContainer = item.GetComponent(); - inventory = itemContainer?.Inventory; - if (!chosenItemSpawnInfo.SpawnIfCantBeContained && !itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + foreach (ItemContainer itemContainer in item.GetComponents()) + { + if (itemContainer.CanBeContained(chosenItemSpawnInfo.ItemPrefab)) + { + inventory = itemContainer?.Inventory; + break; + } + } + if (!chosenItemSpawnInfo.SpawnIfCantBeContained && inventory == null) { return; } @@ -2002,6 +2015,10 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); + if (oneShot) + { + Disabled = true; + } if (Interval > 0.0f && entity != null) { intervalTimers[entity] = Interval; @@ -2175,18 +2192,23 @@ namespace Barotrauma private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) { - float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; - if (entity is Item sourceItem && sourceItem.HasTag("medical")) + float afflictionMultiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem) { - multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user is not null) + if (sourceItem.HasTag("medical")) { - multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + afflictionMultiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + if (user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); + } + } + else if (sourceItem.HasTag(AfflictionPrefab.PoisonType) && user is not null) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - - return multiplier * AfflictionMultiplier; + return afflictionMultiplier * AfflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality) @@ -2196,19 +2218,17 @@ namespace Barotrauma { afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } - if (user is not null) { if (affliction.Prefab.IsBuff) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); } - else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis") + else if (affliction.Prefab.Identifier == "organdamage" && targetCharacter.CharacterHealth.GetActiveAfflictionTags().Any(t => t == "poisoned")) { afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); } } - if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 78cd9451d..e7071ddc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -403,7 +403,7 @@ namespace Barotrauma.Steam var ids = items.Select(it => it.Id.Value).ToHashSet(); var toUninstall = ContentPackageManager.WorkshopPackages .Where(pkg - => !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId) + => !pkg.UgcId.TryUnwrap(out var workshopId) || !ids.Contains(workshopId.Value)) .ToArray(); if (toUninstall.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 09ebf269f..ee733cbe1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -219,10 +219,10 @@ namespace Barotrauma UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } - public static void OnCampaignMetadataSet(Identifier identifier, object value) + public static void OnCampaignMetadataSet(Identifier identifier, object value, bool unlockClients = false) { if (identifier.IsEmpty || value is null) { return; } - UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier()); + UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier(), unlockClients); } public static void OnItemRepaired(Item item, Character fixer) @@ -236,6 +236,15 @@ namespace Barotrauma UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } + public static void OnAfflictionReceived(Affliction affliction, Character character) + { + if (affliction.Prefab.AchievementOnReceived.IsEmpty) { return; } +#if CLIENT + if (GameMain.Client != null) { return; } +#endif + UnlockAchievement(character, affliction.Prefab.AchievementOnReceived); + } + public static void OnAfflictionRemoved(Affliction affliction, Character character) { if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs index 06f36cc62..67b03d501 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -11,6 +11,7 @@ namespace Barotrauma left = l; right = r; } + // TODO: should this be && instead of ||? public override bool Loaded => left.Loaded || right.Loaded; public override void RetrieveValue() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index d41d182b2..b5158393e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -37,8 +37,9 @@ namespace Barotrauma public static float InterpolateRotation(float previous, float current) { + //use a somewhat high epsilon - very small differences aren't visible + if (MathUtils.NearlyEqual(previous, current, epsilon: 0.02f)) { return current; } float angleDiff = MathUtils.GetShortestAngle(previous, current); - return previous + angleDiff * (float)alpha; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index f7b33b9aa..610c74cc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -43,15 +43,32 @@ namespace Barotrauma } } - public int GetBuyPrice(int level, Location? location = null) + public int GetBuyPrice(int level, Location? location = null, ImmutableHashSet? characterList = null) { - int maxLevel = Prefab.GetMaxLevelForCurrentSub(); + float price = BasePrice; - if (level > maxLevel) { maxLevel = level; } + int maxLevel = Prefab.MaxLevel; - int price = BasePrice; - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); - return location?.GetAdjustedMechanicalCost(price) ?? price; + float lerpAmount = maxLevel is 0 + ? level // avoid division by 0 + : level / (float)maxLevel; + + float priceMultiplier = MathHelper.Lerp(IncreaseLow, IncreaseHigh, lerpAmount); + price += price * (priceMultiplier / 100f); + + price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + if (characterList.Any()) + { + if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated)); + } + price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier)); + } + return (int)price; } } @@ -194,11 +211,10 @@ namespace Barotrauma _ => throw new ArgumentOutOfRangeException() }; - public bool AppliesTo(SubmarineInfo sub) + public bool AppliesTo(SubmarineClass subClass, int subTier) { if (type is MaxLevelModType.Invalid) { return false; } - int subTier = sub.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); @@ -211,9 +227,9 @@ namespace Barotrauma return subTier == tier; } - if (tierOrClass.TryGet(out SubmarineClass subClass)) + if (tierOrClass.TryGet(out SubmarineClass targetClass)) { - return sub.SubmarineClass == subClass; + return subClass == targetClass; } return false; @@ -492,15 +508,19 @@ namespace Barotrauma { int level = MaxLevel; - foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) - { - if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } - } + int tier = info.Tier; if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) { int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); - level += modifier; + tier += modifier; + } + + tier = Math.Clamp(tier, 1, SubmarineInfo.HighestTier); + + foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) + { + if (mod.AppliesTo(info.SubmarineClass, tier)) { level = mod.GetLevelAfter(level); } } return level; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index c1225eb7b..a7dedde89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -25,7 +25,7 @@ namespace Barotrauma if (!Done) { Mre.WaitOne(); } } } - private static List enqueuedTasks; + private static readonly List enqueuedTasks; static CrossThread() { enqueuedTasks = new List(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 8b736ad74..76671aea5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -155,7 +155,6 @@ namespace Barotrauma public static float CurveAngle(float from, float to, float step) { - from = WrapAngleTwoPi(from); to = WrapAngleTwoPi(to); @@ -189,13 +188,7 @@ namespace Barotrauma { return 0.0f; } - - while (angle < 0) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.TwoPi) - angle -= MathHelper.TwoPi; - - return angle; + return PositiveModulo(angle, MathHelper.TwoPi); } /// @@ -207,13 +200,9 @@ namespace Barotrauma { return 0.0f; } - // Ensure that -pi <= angle < pi for both "from" and "to" - while (angle < -MathHelper.Pi) - angle += MathHelper.TwoPi; - while (angle >= MathHelper.Pi) - angle -= MathHelper.TwoPi; - - return angle; + float min = -MathHelper.Pi; + float diffFromMin = angle - min; + return diffFromMin - (MathF.Floor(diffFromMin / MathHelper.TwoPi) * MathHelper.TwoPi) + min; } public static float GetShortestAngle(float from, float to) @@ -342,13 +331,13 @@ namespace Barotrauma if (axisAligned1.Y < axisAligned2.Y) { - if (y < axisAligned1.Y) return false; - if (y > axisAligned2.Y) return false; + if (y < axisAligned1.Y) { return false; } + if (y > axisAligned2.Y) { return false; } } else { - if (y > axisAligned1.Y) return false; - if (y < axisAligned2.Y) return false; + if (y > axisAligned1.Y) { return false; } + if (y < axisAligned2.Y) { return false; } } intersection = new Vector2(axisAligned1.X, y); @@ -364,13 +353,13 @@ namespace Barotrauma if (axisAligned1.X < axisAligned2.X) { - if (x < axisAligned1.X) return false; - if (x > axisAligned2.X) return false; + if (x < axisAligned1.X) { return false; } + if (x > axisAligned2.X) { return false; } } else { - if (x > axisAligned1.X) return false; - if (x < axisAligned2.X) return false; + if (x > axisAligned1.X) { return false; } + if (x < axisAligned2.X) { return false; } } intersection = new Vector2(x, axisAligned1.Y); @@ -901,23 +890,30 @@ namespace Barotrauma // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp public static bool NearlyEqual(float a, float b, float epsilon = 0.0001f) { - float diff = Math.Abs(a - b); if (a == b) { - // shortcut, handles infinities + //shortcut, handles infinities return true; } - else if (a == 0 || b == 0 || diff < float.Epsilon) + + if (a == 0 || b == 0) { - // a or b is zero or both are extremely close to it - // relative error is less meaningful here - return diff < epsilon; + //if a or b is zero, relative error is less meaningful + return Math.Abs(a - b) < epsilon; } - else + + float absA = Math.Abs(a); + float absB = Math.Abs(b); + float absAB = absA + absB; + if (absAB < epsilon) { - // use relative error - return diff / (Math.Abs(a) + Math.Abs(b)) < epsilon; + // a and b extremely close to zero, relative error is less meaningful + return true; } + + float diff = Math.Abs(a - b); + // use relative error + return diff / absAB < epsilon; } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index c4ce599c0..9bc971557 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -1,9 +1,7 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using Barotrauma.IO; +using System; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -12,52 +10,6 @@ namespace Barotrauma { public class Md5Hash { - public static class Cache - { - private const string cachePath = "Data/hashcache.txt"; - - private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries - = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); - - public static void Load() - { - if (!File.Exists(cachePath)) { return; } - var lines = File.ReadAllLines(cachePath); - if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) - { - for (int i = 1; i < lines.Length; i++) - { - string[] split = lines[i].Split('|'); - string path = split[0].CleanUpPathCrossPlatform(); - Md5Hash hash = Md5Hash.StringAsHash(split[1]); - DateTime? dateTime = null; - if (long.TryParse(split[2], out long dateTimeUlong)) - { - dateTime = DateTime.FromBinary(dateTimeUlong); - } - - if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) - { - Entries.Add((path, hash, dateTime.Value)); - } - } - } - } - - public static void Add(string path, Md5Hash hash, DateTime dateTime) - { - path = path.CleanUpPathCrossPlatform(); - Remove(path); - Entries.Add((path, hash, dateTime)); - } - - public static void Remove(string path) - { - path = path.CleanUpPathCrossPlatform(); - Entries.RemoveAll(e => e.Path == path); - } - } - public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); private static string RemoveWhitespace(string s) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs deleted file mode 100644 index db6e813b9..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Barotrauma -{ - public sealed class None : Option - { - private None() { } - - public static Option Create() => new None(); - - public override Option Fallback(Option fallback) => fallback; - public override T Fallback(T fallback) => fallback; - - public override bool ValueEquals(T value) => false; - - public override string ToString() - => $"None<{typeof(T).Name}>"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 9aff08c3f..112281e50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,60 +1,83 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; namespace Barotrauma { - /// - /// Implementation of Option type. - /// - /// - /// Credit Jlobblet - /// - public abstract class Option + public readonly struct Option where T : notnull { - public static Option Some(T value) => Some.Create(value); - public static Option None() => None.Create(); - public bool IsNone() => this is None; - public bool IsSome() => this is Some; + private readonly bool hasValue; + private readonly T? value; - public bool TryUnwrap(out T outValue) => TryUnwrap(out outValue); - - public bool TryUnwrap(out T1 outValue) where T1 : T + private Option(bool hasValue, T? value) { - switch (this) - { - case Some { Value: T1 value }: - outValue = value; - return true; - default: - outValue = default!; - return false; - } + this.hasValue = hasValue; + this.value = value; } - public Option Select(Func selector) => - this switch + public bool IsSome() => hasValue; + public bool IsNone() => !IsSome(); + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T + { + bool hasValueOfGivenType = false; + outValue = default; + + if (hasValue && value is T1 t1) { - Some { Value: var value } => Option.Some(selector.Invoke(value)), - None _ => Option.None(), - _ => throw new ArgumentOutOfRangeException() + hasValueOfGivenType = true; + outValue = t1; + } + + return hasValueOfGivenType; + } + + public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue) + => TryUnwrap(out outValue); + + public Option Select(Func selector) where TType : notnull + => TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None; + + public Option Bind(Func> binder) where TType : notnull + => TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None; + + public T Fallback(T fallback) + => TryUnwrap(out var v) ? v : fallback; + + public Option Fallback(Option fallback) + => IsSome() ? this : fallback; + + public static Option Some(T value) + => typeof(T) switch + { + var t when t == typeof(bool) + => throw new Exception("Option type rejects booleans"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>) + => throw new Exception("Option type rejects nested Option"), + {IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>) + => throw new Exception("Option type rejects Nullable"), + _ + => new Option(hasValue: true, value: value ?? throw new Exception("Option type rejects null")) }; - public abstract Option Fallback(Option fallback); - public abstract T Fallback(T fallback); - - public abstract bool ValueEquals(T value); - public override bool Equals(object? obj) => obj switch { - Some { Value: var value } => this is Some { Value: { } selfValue } && selfValue.Equals(value), - None _ => IsNone(), - T value => this is Some { Value: { } selfValue } && selfValue.Equals(value), - _ => false + Option otherOption when otherOption.IsNone() + => IsNone(), + Option otherOption when otherOption.TryUnwrap(out var otherValue) + => ValueEquals(otherValue), + T otherValue + => ValueEquals(otherValue), + _ + => false }; + public bool ValueEquals(T otherValue) + => TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue); + public override int GetHashCode() - => this is Some { Value: { } value } ? value.GetHashCode() : 0; + => TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0; public static bool operator ==(Option a, Option b) => a.Equals(b); @@ -62,22 +85,28 @@ namespace Barotrauma public static bool operator !=(Option a, Option b) => !(a == b); - public abstract override string ToString(); - - public static implicit operator Option(Option.UnspecifiedNone _) + public static Option None() + => default; + + public static implicit operator Option(in Option.UnspecifiedNone _) => None(); + + public override string ToString() + => TryUnwrap(out var selfValue) + ? $"Some<{typeof(T).Name}>({selfValue})" + : $"None<{typeof(T).Name}>"; } public static class Option { - public sealed class UnspecifiedNone + public static Option Some(T value) where T : notnull + => Option.Some(value); + + public static UnspecifiedNone None + => default; + + public readonly ref struct UnspecifiedNone { - private UnspecifiedNone() { } - internal static readonly UnspecifiedNone Instance = new(); } - - public static UnspecifiedNone None => UnspecifiedNone.Instance; - - public static Option Some(T value) => Option.Some(value); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs deleted file mode 100644 index 5fd1dc3b0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Barotrauma -{ - public sealed class Some : Option - { - public readonly T Value; - - private Some(T value) - { - if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } - Value = value; - } - - public static Option Create(T value) => new Some(value); - - public override Option Fallback(Option fallback) => this; - public override T Fallback(T fallback) => Value; - - public override bool ValueEquals(T value) => Value.Equals(value); - - public override string ToString() - => $"Some<{typeof(T).Name}>({Value})"; - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index 8b9396787..f727db4eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -65,7 +65,7 @@ namespace Barotrauma /// Assembly to remove. public static void RemoveAssemblyFromCache(Assembly assembly) => cachedNonAbstractTypes.Remove(assembly); - public static Option ParseDerived(TInput input) where TInput : notnull + public static Option ParseDerived(TInput input) where TInput : notnull where TBase : notnull { static Option none() => Option.None(); @@ -96,10 +96,19 @@ namespace Barotrauma f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs); return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) }) - as Option ?? none(); + as Option? ?? none(); } - return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none(); + return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()); + } + + public static string NameWithGenerics(this Type t) + { + if (!t.IsGenericType) { return t.Name; } + + string result = t.Name[..t.Name.IndexOf('`')]; + result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>"; + return result; } public static string NameWithGenerics(this Type t) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs index 9f65e0ef8..49c50dc1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SerializableDateTime.cs @@ -141,9 +141,8 @@ namespace Barotrauma public SerializableDateTime ToLocal() => new SerializableDateTime( - DateTime.SpecifyKind( - value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, - DateTimeKind.Local)); + new DateTime(ticks: value.Ticks) - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value, + SerializableTimeZone.LocalTimeZone); public long Ticks => value.Ticks; diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 8f63361d2..0c984657f 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,226 @@ +--------------------------------------------------------------------------------------------------------- +v1.0.7.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed mechanic tutorial getting stuck at the point where you need to weld a leak. +- Fixed treatment suggestions not showing up in the naloxone part of the medic tutorial. +- Fixed patient spawning dead in the CPR part of the medic tutorial. +- Fixed bots being unable to move through Herja's airlock due to an unlinked waypoint. +- Fixed tutorial Dugong spawning with empty ammo boxes. + +--------------------------------------------------------------------------------------------------------- +v1.0.6.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed some new Steam achievements not unlocking. + +--------------------------------------------------------------------------------------------------------- +v1.0.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixes to Japanese translations. +- Implemented support for some upcoming Steam achievements. +- Improved backwards compatibility: fixed outpost managers no longer spawning in mods that override outpost generation parameters due to the generic non-faction-specific outpost manager prefab being removed. + +--------------------------------------------------------------------------------------------------------- +v1.0.4.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed outpost NPCs getting randomized every time you re-enter an outpost. +- Fixed inability to gain more than 15 talent points in the multiplayer campaign. + +--------------------------------------------------------------------------------------------------------- +v1.0.3.0 +--------------------------------------------------------------------------------------------------------- + +- Adjusted plant spawn rates in caves. +- Made lead more common in stores. +- Fixed mission listing shown on the top of the screen spoiling enemy faction ambushes. +- Fixed enemy faction ambushes being possible everywhere on the map (they should only happen within 3 steps of outposts belonging to an enemy faction). +- Fixes bots sometimes running towards doors that are closed by something else (another person or an automatic logic). +- Fixes bots ordered to wait outside of the submarine not being able to switch their oxygen tanks. +- Added a couple of endgame-foreshadowing lore bits. + +--------------------------------------------------------------------------------------------------------- +v1.0.2.0 +--------------------------------------------------------------------------------------------------------- + +- Exosuits are powered by fuel rods instead of batteries. +- Fixed affliction probabilities being evaluated twice, meaning that e.g. 50% probability of getting some affliction from an attack was actually a 25% probability. +- Fixed item highlights from the previous round remaining visible the next round. +- Fixed swapping items in a container sometimes causing too many items to be visible in it. +- Fix the vitality modifiers on husk not working properly, because health indices on the limbs were not defined. Effectively husks always took 2x damage. +- Fixed characters still spawning inside outposts that have turned hostile due to low reputation. +- Fixed all special faction hire events getting stuck if you say you need to "think about it", return, and say you still need to think about it. +- Fixed missing "place in ceiling" text in beacon station save dialog. +- Fixed basic depth charges being cheaper than intended (only 30 mk). +- Fixed inability to make lights blink at a high frequency by rapidly turning them on and off with e.g. oscillators. +- Fixed red glow around the light switch's green button. +- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. +- Fixed "skedaddle" not giving a 10% movement boost like the description says. +- Fixed acid burns not having a cause of death text. +- Fixed ranged weapons emitting particles in the wrong direction. There haven't been any changes to this code in years, so it must've been an issue for a long time, I guess we just never noticed because no gun before the scrap cannon emitted particles with a noticeable velocity? +- Fixed banana being held weirldy. +- Fixed a pathfinding issue that often made bots swim against cave walls. +- Fixed inability to join servers with a submarine switch/purchase vote running. +- Fixed votes passing if the client who initiated them disconnects before anyone else votes. +- Fixed follow orders not being persistent between singleplayer rounds. + +--------------------------------------------------------------------------------------------------------- +v1.0.1.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed Mailman talent giving you the 150 mk bonus every time you open the campaign map or mission menu and there's a cargo mission visible. +- Fixed missions available from the destination location to some other location being listed as "outpost missions" in the campaign map's mission selection. +- Fixed outposts that faction missions take place in being allowed to turn into abandoned outposts when next to hunting grounds, making the missions impossible to complete. +- Fixed hidden items (e.g. Separatist deco that's disabled in Coalition outposts) sometimes getting chosen as targets for scripted events, resulting in non-interactable, glowing "ghost items". +- Fixed mysterious floating status monitor in AdminModule_02_Colony. +- Miscellaneous optimizations. +- Fixed Separatist jailbreak mission causing a crash. +- Fixed ranged weapons (most noticeably, scrap cannon) emitting particles in the wrong direction. +- Fixed acid burns not having a cause of death text. +- Fixed "skedaddle" not giving a 10% movement boost like the description says. +- Fixed odd fabrication list sorting: the items that require a recipe to fabricate were split into ones you have the skills to fabricate and ones you don't, even though that isn't visible in the UI, making the list just seem out of order. +- Fixed red glow around the light switch's green button. +- Fixed banana being held weirldy. +- Fixed inability to hold a captain's pipe or cigar in your left hand. +- Fixed ready checks not working. + +--------------------------------------------------------------------------------------------------------- +v1.0.0.0 +--------------------------------------------------------------------------------------------------------- + +Faction overhaul: +- Outposts are controlled by the Europa Coalition or Jovian Separatists, and some of them include a module belonging to the Church of Husk or Children of the Honkmother. +- Got rid of location-specific reputation. Now all the events/missions give faction reputation instead (excluding missions that aren't related to or given by a faction, e.g. abandoned outpost missions). +- Lots of new outpost events, and a longer "event chain" for the secondary factions. +- Lots of new faction-specific missions: some variants of existing missions, some new. +- Faction-specific hires: "generic" high-level characters with more experience points and better gear than normal hireable NPCs. Available for hiring when Coalition or Separatist reputation is high enough. +- Special, named characters who can be hired via scripted events after reaching a high enough reputation. +- Faction-specific vendors (separatists, husks, clowns) who sell special items (many of which are completely new) if your reputation is high enough. +- If your Coalition/Separatist reputation is low enough, you may get attacked by their vessel during missions. +- There's now always two paths from biome to another, one controlled by the Coalition and one by the Separatists. +- Improvements to the campaign map. +- Added a 3rd talent tree, "Politician", for the Captain. Focused around faction relations and reputation. + +Endgame: +- Completely remade the ending of the campaign. Now you'll get to see what's beyond the Eye of Europa and perhaps uncover the cause for the increasing levels or radiation. +- New types of enemies/bosses. +- Some new events to foreshadow the ending during the course of the campaign. + +Misc changes: +- New loading screen / location portraits. +- Two new music tracks. +- Items' skill requirements are shown in their tooltips (the same way as damage resistances). +- Tweaks to poisons. +- Adjusted Europan Handshake to work better with the overhauled morbusine poisoning. +- Acid Grenades and 40mm Acid Grenades are now properly affected by talents +- Acid Grenades and 40mm Acid Grenades deal more damage and slow enemies down, making them more viable against fast monsters. +- Made regular 40mm grenades penetrate armor more efficiently. +- Made Diving Suits resist Acid Burns a bit more. +- Europa Brew's Acid Vulnerability is now double as effective (200% damage taken instead of 100%). +- Adjustment to throwable items (shorter throw distance and reduced speed in water). +- Made flares float in place to make them more useful. +- Made high-quality stun guns more effective (stunning the target faster). +- The health scanner always shows poisons and paralysis on monsters to make it easier to determine whether the poisoning is progressing or wearing off. +- A pass on sound ranges: the ranges should now be more consistent and sensible. +- Made moloch shell fragment and riot shield medium items instead of small to fix them going inside e.g. toolbelts. +- Made husk eggs consumable. +- Made it more difficult to repeatedly enter an abandoned outpost and re-loot the bandits: now the bandits immediately attack you if you re-enter the outpost. +- Monsters you haven't encountered yet are now hidden by default in the character editor. Can be enabled using the command "showmonsters" and re-hidden using "hidemonsters". The value is saved in creaturemetrics.xml. Doesn't affect custom creatures. +- Fixed character crush depths behaving inconsistently (varying between levels, e.g. sometimes crushing the character at the depth of 2000 meters, sometimes 3000). +- Improvements to submarine crush depth effects: previously the breaches were easy to deal with because pressure did small amounts of damage to all walls, now it instead does heavier damage to some walls (and the amount of damage and walls to damage increases with depth). +- Added a round light component variant. +- Increased the hard-coded max mission count back from 3 to 10. It'd be preferable to not change the value above 3 in the vanilla game, but since campaign settings are not moddable, we shouldn't be too strict about it (because it can be useful for a mod that this value can be adjusted). +- Miscellaneous optimizations. +- New slot indicator icons (= the icons that show what can go inside some items, like tanks/ammo). +- Made outpost hull repair service cheaper. +- Doors can now be damaged by melee weapons and ranged (handheld) weapons. (They were already destructible by submarine mounted weapons and explosives) +- Adjusted and rebalanced item damage for most items, to take into account doors being destructible. +- Reduced time needed for a crowbar to open doors, 7.5s for regular doors, 6s for wrecked doors (down from 10 s). +- Boosted Plasma Cutter damage against doors and items (walls not touched). +- Made galena more common in order to make lead easier to get. +- Added Auto Operate option for all turrets. Can be enabled in the submarine editor. Not currently used on vanilla submarines. Auto operated turrets don't require a person to operate them, but they still require power and ammunition (-> someone needs to reload them). + +Multiplayer: +- Added a language filter to the server browser. +- Fixed reports given by dragging and dropping them on the status monitor always targeting the room the character is inside. +- Improvements to medical clinic syncing (should fix some of the afflictions a character has sometimes not being visible on the list). +- Fixed crashing if you close a server when mod downloads are disabled. +- Improved projectile syncing: spread now behaves the same client-side as it does server-side (as opposed to being completely random). +- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen. +- Fixed server randomizing the game mode at the end of the round when playing a campaign with the game mode selection set to Random. + +AI: +- Fixed bots considering certain multi-hull rooms flooded when they are not. +- Fixed bots deciding prematurely that they can't fix an item when it's deteriorating (e.g. when it's submerged). +- Fixed bots removing battery cells from exosuits when ordered to charge batteries. +- Fixed bots sometimes getting stuck in automatic doors and/or double doors, because they didn't wait for the door to open entirely before pressing the button again. +- Improved bot ‘extinguish fires’ behavior. Fixes bots sometimes not being able to extinguish larger fires, because they stopped too far and didn't keep advancing towards the target. +- Fixed bots claiming that they can't return back to the sub and then following the order anyway. +- Improved the ‘find safety’ calculations so that the bots give more preference to the distance of the room. +- Fixed some remaining issues and edge cases in the logic over when the bot needs diving gear and when it can be taken off. +- Fixed captains (and some NPCs) idling in the airlock if they equip a diving suit. +- Bot can now target items (like projectiles) with turrets and have different targeting priorities on different monsters. +- Fixed bots being allowed to reach items that are too far to be interacted with. + +Fixes: +- Fixes and improvements to translations (Japanese and Chinese in particular). +- Fixed light components with a range of 0 and a hidden sprite being invisible against dark backgrounds. +- Various fixes to Typhon 1: most notably, adjusting the hulls to prevent some rooms from being impossible to drain fully. +- Fixed "kill" command not killing characters under the influence of "Miracle Worker". +- Fixed some lights becoming invisible when their range is set to 0 and they're against a dark background. +- Fixed lights turning on without power when they receive a toggle or set_color input. +- Fixed changing the amount of items to fabricate inadvertently starting(or activating) fabrication in MP if you've previously started fabricating something +- Fixed campaign settings resetting in the campaign setup menu every time you relaunch the game (meaning you'd always need to e.g. remember to toggle the tutorial off if you want to play without it). +- Fixed inverted mouse buttons not working properly since the last update: the left mouse button was considered the primary mouse button regardless of your OS settings. +- Fixed status monitor not properly displaying condition on tinkered items. +- Fixed machines smoking when above 100% condition with tinkering. +- Fixed inventory overlapping with the chatbox on low aspect ratios (small width, large height). +- Fixed some layering issues in abandoned outposts. +- Fixed water-sensitive items sometimes spawning as loot in wrecks. +- Fixed radio static still playing even if you don't have a headset. +- Fixed rifle grenade sounds not working. +- Fixed crashing on startup if the MD5 hash cache file is empty. +- Fixed research stations and loaders not being visible on the status monitor's electrical view. +- Fixed artifact missions sometimes choosing the same artifact as a target if you happen to have multiple missions active at a time, which would lead to console errors when the round ends. +- Fixed exosuit playing the warning beep if there's empty or almost empty tanks in any of its slots. +- Fixed oxygen generators deteriorating in some of the outpost modules. +- Fixed reputation loss when a character other than the player (e.g. crawlers in the 'crawleroutbreak' event) damages the outpost walls. +- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub. +- Fixed characters trying to walk in flooded spaces that are too low to stand in (like some of the tight passages in alien ruins). +- Genetic material backwards compatibility to fix old unidentified genetic materials disappearing from saves prior to v0.21.6.0. +- Fixed genetic materials being too rare in outposts now. +- Fixed hunting grounds affecting outposts 2 steps away, not just ones in adjacent locations. +- Fixed "residual waste" talent duplicating genetic materials. +- Fixed monsters sometimes spawning inside destructible ice chunks in caves. +- Fixed respawn shuttle sometimes spawning inside floating ice chunks. +- Fixed equipping a ranged weapon setting its reload timer to 1, making it possible to reduce some weapons' loading times by unequipping and equipping them. +- Fixed partially consumed items not staying on top of the stack they're in. +- Fixed submarine tier and class affecting the prices of the submarine upgrades: e.g. a tier 2 upgrade would cost more on a submarine where tier 2 is the maximum than on a submarine with a higher maximum. +- Fixed reputation loss when you steal items from bandits in a beacon station. +- Fixed equipped flares igniting when you click on the inventory. +- Fixed "Quickdraw" talent not affecting Alien Pistols. +- Fixed toolbelts and other items worn on the torso getting hidden when wearing a safety harness. +- Fixed advanced syringe gun and slipsuit fabrication recipes. +- Fixed floating pumps and ladder layering issues in Herja. +- Limited the number of makeshift shelves per sub to 3 (similar to portable pumps). Otherwise you can use them to expand the sub's cargo capacity indefinitely. +- Fixed dementonite and hardened crowbars spawning in respawn containers (= respawn shuttle cabinets). +- Fixed alien blood no longer causing psychosis, + made it slightly less effective to make fabricating blood packs more worthwhile +- Fixed fire extinguisher spray getting blocked by characters. +- Venture: Fixed the battery room not flooding properly (again), fixed the two hulls in the airlock not being linked, adjusted the waypoints a bit. +- Selkie: Disconnect the outer nodes from the ladder/door nodes, because the docking ports can't be opened manually. +- Fixed Thalamus AI not running properly when there's no player characters or submarines around (e.g. when all the players are in the freecam mode). +- Fixed the ammo indicator not showing correctly on the advanced syringe gun. +- Fixed bots sometimes getting confused by outside waypoints while being inside an outpost. +- Fixed item relocation logic running also on NPCs that are not in the player team, which could cause diving suits dropped by NPCs to get spawned in the player sub. + +Modding: +- Fixed crashing if a StatusEffect is configured to SpawnItemRandomly but doesn't configure anything to spawn. +- Improved the error handling of item/character variants. Previously if the parent prefab wasn't found, there was no error message, but the variant was still created, causing crashes in various situations. +- Added AITurretPriority and AISlowTurretPriority on items and characters. Setting the priority to 0 can be used for telling the bots to ignore the target entirely. Items also need to have IsAITurretTarget="True" enabled to make them a valid target. +- Added ItemDamageMultiplier on items. Can be used for increasing the damage caused by other items, like weapons. Works like the existing ExplosionDamageMultiplier. + --------------------------------------------------------------------------------------------------------- v0.21.6.0 --------------------------------------------------------------------------------------------------------- @@ -5,6 +228,7 @@ v0.21.6.0 - Minor localization fixes. - Fixed some occasional crashes in the character editor. + --------------------------------------------------------------------------------------------------------- v0.21.5.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaTest/EndpointParseTests.cs b/Barotrauma/BarotraumaTest/EndpointParseTests.cs index c8ccb4e43..0038d952b 100644 --- a/Barotrauma/BarotraumaTest/EndpointParseTests.cs +++ b/Barotrauma/BarotraumaTest/EndpointParseTests.cs @@ -14,8 +14,7 @@ public class EndpointParseTests { Endpoint.Parse("127.0.0.1:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -25,8 +24,7 @@ public class EndpointParseTests { Endpoint.Parse("localhost:27015") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new LidgrenEndpoint(IPAddress.Loopback, 27015)), options => options.RespectingRuntimeTypes()); } @@ -36,8 +34,7 @@ public class EndpointParseTests { Address.Parse("127.0.0.1") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new LidgrenAddress(IPAddress.Loopback)), options => options.RespectingRuntimeTypes()); } @@ -47,8 +44,7 @@ public class EndpointParseTests { Endpoint.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option.Some(new SteamP2PEndpoint(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); } @@ -58,8 +54,7 @@ public class EndpointParseTests { Address.Parse("STEAM_1:1:508792388") .Should() - .BeOfType>() - .And.BeEquivalentTo( + .BeEquivalentTo( Option
.Some(new SteamP2PAddress(new SteamId(76561198977850505))), options => options.RespectingRuntimeTypes()); new SteamId(76561198977850505).StringRepresentation.Should().BeEquivalentTo("STEAM_1:1:508792388"); diff --git a/Barotrauma/BarotraumaTest/MathUtilsTests.cs b/Barotrauma/BarotraumaTest/MathUtilsTests.cs new file mode 100644 index 000000000..e624b3f4e --- /dev/null +++ b/Barotrauma/BarotraumaTest/MathUtilsTests.cs @@ -0,0 +1,67 @@ +using Barotrauma; +using FluentAssertions; +using Microsoft.Xna.Framework; +using System; +using Xunit; + +namespace TestProject; + +public class MathUtilsTests +{ + [Fact] + public void TestNearlyEquals() + { + MathUtils.NearlyEqual(0.0f, 0.0f).Should().BeTrue(); + MathUtils.NearlyEqual(-float.Epsilon, float.Epsilon).Should().BeTrue(); + MathUtils.NearlyEqual(0.1f + 0.2f, 0.3f).Should().BeTrue(); + MathUtils.NearlyEqual(-1.0f, 1.0f).Should().BeFalse(); + } + + [Fact] + public void TestWrapAngle() + { + MathUtils.NearlyEqual(MathUtils.WrapAnglePi(0.0f), 0.0f).Should().BeTrue(); + + CheckWrapAnglePiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, -90).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-90, 90).Should().BeFalse(); + CheckWrapAnglePiNearlyEqual(-180, 180).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-190.0f, 170.0f).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(-360, 0).Should().BeTrue(); + CheckWrapAnglePiNearlyEqual(360, 0).Should().BeTrue(); + + bool CheckWrapAnglePiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckWrapAngleTwoPiNearlyEqual(0, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(90, 90).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-90, 270).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(180, 180).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(360 * 5, 0).Should().BeTrue(); + CheckWrapAngleTwoPiNearlyEqual(-360, 0).Should().BeTrue(); + + bool CheckWrapAngleTwoPiNearlyEqual(float wrappedDeg, float deg) + { + float wrappedRad = MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(wrappedDeg)); + float rad = MathHelper.ToRadians(deg); + return MathUtils.NearlyEqual(wrappedRad, rad) || MathUtils.NearlyEqual(Math.Abs(wrappedRad - rad), MathHelper.TwoPi); + } + + CheckShortestAngleNearlyEqual(0.0f, 0.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 90.0f, 90.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, 360.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(0.0f, -365.0f, -5.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(180.0f, -180.0f, 0.0f).Should().BeTrue(); + CheckShortestAngleNearlyEqual(-355.0f, 5.0f, 10.0f); + + bool CheckShortestAngleNearlyEqual(float deg1, float deg2, float angle) + { + return MathUtils.NearlyEqual(MathUtils.GetShortestAngle(MathHelper.ToRadians(deg1), MathHelper.ToRadians(deg2)), MathHelper.ToRadians(angle)); + } + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs index 12cdb9243..03cbcb402 100644 --- a/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs +++ b/Barotrauma/BarotraumaTest/SerializableDateTimeTests.cs @@ -37,22 +37,26 @@ public sealed class SerializableDateTimeTests { Prop.ForAll(EqualityCheck).QuickCheckThrowOnFailure(); } - + [Fact] public void ParseTest() { - var parseTest = "9369Y 09M 06D 03HR 43MIN 09SEC UTC+8:49"; - SerializableDateTime.Parse(parseTest); Prop.ForAll(ParseCheck).QuickCheckThrowOnFailure(); } - + + [Fact] + public void ToLocalTest() + { + Prop.ForAll(ToLocalCheck).QuickCheckThrowOnFailure(); + } + private static void EqualityCheck(SerializableDateTime original) { var local = original.ToLocal(); var utc = original.ToUtc(); - original.Should().BeEquivalentTo(local); - original.Should().BeEquivalentTo(utc); - local.Should().BeEquivalentTo(utc); + original.Should().BeEquivalentTo(local, because: "original must equal local"); + original.Should().BeEquivalentTo(utc, because: "original must equal utc"); + local.Should().BeEquivalentTo(utc, because: "local must equal utc"); } private static void ParseCheck(SerializableDateTime original) @@ -61,4 +65,11 @@ public sealed class SerializableDateTimeTests SerializableDateTime.Parse(str).TryUnwrap(out var parsedTime).Should().BeTrue(); parsedTime.Should().BeEquivalentTo(original); } + + private static void ToLocalCheck(SerializableDateTime original) + { + var localNow = SerializableDateTime.LocalNow; + var convertedDateTime = original.ToLocal(); + localNow.TimeZone.Should().BeEquivalentTo(convertedDateTime.TimeZone); + } }