diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 0bfac9a82..46589743e 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 ed7ff67d7..6f588373e 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; @@ -45,8 +48,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() @@ -66,7 +80,7 @@ namespace Barotrauma { 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 +94,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 +152,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 @@ -164,9 +179,16 @@ namespace Barotrauma yield return CoroutineStatus.Running; } - Running = false; + float endTimer = 0.0f; + while (endTimer <= EndWaitDuration) + { + cam.Translate(endPos - cam.Position); + cam.Zoom = endZoom; + endTimer += CoroutineManager.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/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index c43d3f26b..56cb2ac83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -47,8 +47,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity}", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"{targetValue.FormatZeroDecimal()} (M: {SelectedTargetMemory?.Priority.FormatZeroDecimal()}, P: {SelectedTargetingParams?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index dc50608c8..cd3614aea 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 ? 0.01f : 0.2f; @@ -442,8 +444,7 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; } - + if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; } Vector2 spriteOrigin = limb.ActiveSprite.Origin; spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; limb.ActiveSprite.Origin = spriteOrigin; @@ -468,7 +469,10 @@ namespace Barotrauma { var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10); - SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); + if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase)) + { + SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 890be7cb1..a350ef029 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -42,7 +42,7 @@ namespace Barotrauma if (sound != null) { - SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling); + SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 99557594c..0e5315390 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,42 @@ 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 + /// + public float CameraShake + { + get { return Screen.Selected?.Cam?.Shake ?? 0.0f; } + set + { + if (!MathUtils.IsValid(value)) { return; } + if (Screen.Selected?.Cam != null) + { + Screen.Selected.Cam.Shake = value; + } + } + } + private readonly List bloodEmitters = new List(); public IEnumerable BloodEmitters { @@ -907,7 +943,7 @@ namespace Barotrauma { name += " " + TextManager.Get("Disguised"); } - else if (Info.Title != null) + else if (Info.Title != null && TeamID != CharacterTeamType.Team1) { name += '\n' + Info.Title; } @@ -965,14 +1001,31 @@ namespace Barotrauma } if (IsDead) { return; } - - if (CharacterHealth.DisplayedVitality < MaxVitality * 0.98f && hudInfoVisible) + + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode != EnemyHealthBarMode.ShowAll) + { + if (Controlled == null) + { + if (!IsOnPlayerTeam) { return; } + } + else + { + if (!HumanAIController.IsFriendly(Controlled, this) || + (AIController is HumanAIController humanAi && humanAi.ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective && HumanAIController.IsFriendly(Controlled, combatObjective.Enemy))) + { + return; + } + } + } + + 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 f4742ac64..fb10600e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Tutorials; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -14,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; @@ -25,9 +23,14 @@ 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 BossProgressBar(LocalizedString label) { - Character = character; FadeTimer = BossHealthBarDuration; TopContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.18f, 0.03f), HUDFrame.RectTransform, Anchor.TopCenter) @@ -35,7 +38,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) @@ -48,7 +51,7 @@ namespace Barotrauma { 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 @@ -60,6 +63,50 @@ namespace Barotrauma SideContainer.CanBeFocused = false; SideContainer.Children.ForEach(c => c.CanBeFocused = false); } + + public abstract bool IsDuplicate(BossProgressBar progressBar); + } + + 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 BossHealthBar(Character character) : base(character.DisplayName) + { + Character = character; + } + + public override bool IsDuplicate(BossProgressBar progressBar) + { + return progressBar is BossHealthBar bossHealthBar && bossHealthBar.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; + + public MissionProgressBar(Mission mission) : base(mission.Prefab.ProgressBarLabel) + { + Mission = mission; + } + + public override bool IsDuplicate(BossProgressBar progressBar) + { + return progressBar is MissionProgressBar missionProgressBar && missionProgressBar.Mission == Mission; + } } private static readonly Dictionary orderIndicatorCount = new Dictionary(); @@ -70,7 +117,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 bossHealthBars = new List(); private static readonly Dictionary cachedHudTexts = new Dictionary(); @@ -114,7 +161,7 @@ namespace Barotrauma return character?.Inventory != null && !character.Removed && !character.IsKnockedDown && - (controller?.User != character || !controller.HideHUD) && + (controller?.User != character || !controller.HideHUD || Screen.Selected.IsEditor) && !IsCampaignInterfaceOpen && !ConversationAction.FadeScreenToBlack; } @@ -159,7 +206,7 @@ namespace Barotrauma public static void Update(float deltaTime, Character character, Camera cam) { - UpdateBossHealthBars(deltaTime); + UpdateBossProgressBars(deltaTime); if (GUI.DisableHUD) { @@ -307,7 +354,7 @@ namespace Barotrauma { if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); - if (alpha <= 0.0f) continue; + if (alpha <= 0.0f) { continue; } GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } @@ -548,7 +595,7 @@ namespace Barotrauma if (CharacterHealth.OpenHealthWindow == character.SelectedCharacter.CharacterHealth) { character.SelectedCharacter.CharacterHealth.Alignment = Alignment.Left; - character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); + //character.SelectedCharacter.CharacterHealth.DrawStatusHUD(spriteBatch); } } else if (character.Inventory != null) @@ -602,7 +649,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; @@ -640,20 +689,41 @@ namespace Barotrauma } } - public static void ShowBossHealthBar(Character character) + public static void ShowBossHealthBar(Character character, float damage) { if (character == null || character.IsDead || character.Removed) { return; } + AddBossProgressBar(new BossHealthBar(character), damage); + } - var existingBar = bossHealthBars.Find(b => b.Character == character); - if (existingBar != null) + public static void ShowMissionProgressBar(Mission mission) + { + if (mission == null || mission.Completed || mission.Failed) { return; } + AddBossProgressBar(new MissionProgressBar(mission)); + } + + private static void AddBossProgressBar(BossProgressBar progressBar, float damage = 0.0f) + { + var healthBarMode = GameMain.NetworkMember?.ServerSettings.ShowEnemyHealthBars ?? GameSettings.CurrentConfig.ShowEnemyHealthBars; + if (healthBarMode == EnemyHealthBarMode.HideAll) { - existingBar.FadeTimer = BossHealthBarDuration; return; } + var existingBar = bossHealthBars.Find(b => b.IsDuplicate(progressBar)); + if (existingBar != null) + { + existingBar.FadeTimer = BossHealthBarDuration; + if (damage > 0) + { + // Show the most recent target at the top of the screen + bossHealthBars.Remove(existingBar); + bossHealthBars.Add(existingBar); + } + return; + } if (bossHealthBars.Count > 5) { - BossHealthBar oldestHealthBar = bossHealthBars.First(); + BossProgressBar oldestHealthBar = bossHealthBars.First(); foreach (var bar in bossHealthBars) { if (bar.TopHealthBar.BarSize < oldestHealthBar.TopHealthBar.BarSize) @@ -663,54 +733,65 @@ namespace Barotrauma } oldestHealthBar.FadeTimer = Math.Min(oldestHealthBar.FadeTimer, 1.0f); } - - bossHealthBars.Add(new BossHealthBar(character)); + bossHealthBars.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++) { var bossHealthBar = bossHealthBars[i]; - bool showTopBar = i == 0; - if (showTopBar != bossHealthBar.TopContainer.Visible) + bool showTopBar = i == bossHealthBars.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(component.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; - - 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--) { var bossHealthBar = bossHealthBars[i]; - if (bossHealthBar.FadeTimer <= 0) + if (bossHealthBar.FadeTimer <= 0 || healthBarMode == EnemyHealthBarMode.HideAll) { bossHealthBar.SideContainer.Parent?.RemoveChild(bossHealthBar.SideContainer); bossHealthBar.TopContainer.Parent?.RemoveChild(bossHealthBar.TopContainer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index ab2942b42..c2d6b2e4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -78,7 +78,7 @@ namespace Barotrauma Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero @@ -92,8 +92,8 @@ namespace Barotrauma } if (Job != null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) { Padding = Vector4.Zero }; @@ -101,7 +101,7 @@ namespace Barotrauma if (PersonalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), PersonalityTrait.DisplayName), font: font) { @@ -109,7 +109,23 @@ namespace Barotrauma }; } - if (Job != null && (Character == null || !Character.IsDead)) + GUIButton manageTalentButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), headerTextArea.RectTransform), + text: TextManager.Get("ClientPermission.ManageBotTalents"), style: "GUIButtonSmall") + { + Enabled = false, + UserData = TalentMenu.ManageBotTalentsButtonUserData, + TextBlock = + { + AutoScaleHorizontal = true + } + }; + + if (TalentMenu.CanManageTalents(this)) + { + manageTalentButton.Enabled = true; + } + + if (Job != null && Character is not { IsDead: true }) { var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { @@ -120,7 +136,7 @@ namespace Barotrauma skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; - + foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); @@ -144,7 +160,7 @@ namespace Barotrauma } } } - else if (Character != null && Character.IsDead) + else if (Character is { IsDead: true }) { var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) { @@ -524,25 +540,33 @@ 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); 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 4a9a99547..8d5e63f26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -501,7 +501,7 @@ namespace Barotrauma info?.ClearSavedStatValues(statType); for (int i = 0; i < savedStatValueCount; i++) { - string statIdentifier = msg.ReadString(); + Identifier statIdentifier = msg.ReadIdentifier(); float statValue = msg.ReadSingle(); bool removeOnDeath = msg.ReadBoolean(); info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 983906c72..05903b126 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -32,12 +32,6 @@ namespace Barotrauma public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; - private readonly static LocalizedString[] strengthTexts = new LocalizedString[] - { - TextManager.Get("AfflictionStrengthLow"), - TextManager.Get("AfflictionStrengthMedium"), - TextManager.Get("AfflictionStrengthHigh") - }; private Point screenResolution; @@ -89,11 +83,23 @@ namespace Barotrauma private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; + /// + /// Container for the icons above the health bar + /// + private GUIComponent afflictionIconContainer; + + private GUIButton showHiddenAfflictionsButton; + + /// + /// Container for passive afflictions that have been hidden from afflictionIconContainer + /// + private GUIComponent hiddenAfflictionIconContainer; + private GUIProgressBar healthWindowHealthBar; private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; - private GUIListBox afflictionIconContainer; + private GUIListBox afflictionIconList; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; @@ -331,7 +337,7 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); + afflictionIconList = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); @@ -379,6 +385,25 @@ namespace Barotrauma Enabled = true }; + afflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + + showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular") + { + CanBeFocused = false + }; + + hiddenAfflictionIconContainer = new GUILayoutGroup( + HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = GUI.IntScale(5) + }; + UpdateAlignment(); SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) @@ -596,7 +621,7 @@ namespace Barotrauma public void UpdateHUD(float deltaTime) { - if (GUI.DisableHUD) return; + if (GUI.DisableHUD) { return; } if (openHealthWindow != null) { if (openHealthWindow != Character.Controlled?.CharacterHealth && openHealthWindow != Character.Controlled?.SelectedCharacter?.CharacterHealth) @@ -700,6 +725,8 @@ namespace Barotrauma distortTimer = 0.0f; } + UpdateStatusHUD(deltaTime); + if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && Character.Controlled.AllowInput && !toggledThisFrame) { @@ -726,9 +753,9 @@ namespace Barotrauma OpenHealthWindow = null; } - foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) + foreach (GUIComponent afflictionIcon in afflictionIconList.Content.Children) { - if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } + if (afflictionIcon.UserData is not Affliction affliction) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { afflictionIcon.Flash(GUIStyle.Red); @@ -900,7 +927,7 @@ namespace Barotrauma healthBarHolder.CanBeFocused = healthBar.CanBeFocused = healthBarShadow.CanBeFocused = !Character.ShouldLockHud(); if (Character.AllowInput && UseHealthWindow && !Character.DisableHealthWindow && healthBar.Enabled && healthBar.CanBeFocused && - (GUI.IsMouseOn(healthBar) || highlightedAfflictionIcon != null) && Inventory.SelectedSlot == null) + (GUI.IsMouseOn(healthBar) || GUI.MouseOn?.UserData is AfflictionPrefab) && Inventory.SelectedSlot == null) { healthBar.State = GUIComponent.ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -960,7 +987,12 @@ namespace Barotrauma } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) { - healthBarHolder.AddToGUIUpdateList(); + healthBarHolder.AddToGUIUpdateList(); + afflictionIconContainer.AddToGUIUpdateList(); + if (hiddenAfflictionIconContainer.Visible) + { + hiddenAfflictionIconContainer.AddToGUIUpdateList(); + } } if (SuicideButton.Visible && Character == Character.Controlled) { @@ -989,7 +1021,7 @@ namespace Barotrauma if (affliction.Prefab.AfflictionOverlay != null) { Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay; - ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * (affliction.GetAfflictionOverlayMultiplier()), Vector2.Zero, 0.0f, + ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f, new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y)); } } @@ -1021,94 +1053,134 @@ namespace Barotrauma // If manning a turret the portrait doesn't get rendered so we push the health bar to remove the empty gap healthBarHolder.RectTransform.ScreenSpaceOffset = Character.ShouldLockHud() ? new Point(0, HUDLayoutSettings.PortraitArea.Height) : Point.Zero; } - - DrawStatusHUD(spriteBatch); } - private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; - public void DrawStatusHUD(SpriteBatch spriteBatch) + //private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; + + private readonly List statusIcons = new List(); + private readonly Dictionary statusIconVisibleTime = new Dictionary(); + private const float HideStatusIconDelay = 5.0f; + + public void UpdateStatusHUD(float deltaTime) { - highlightedAfflictionIcon = null; - //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - var statusIcons = new List<(Affliction Affliction, LocalizedString Warning)>(); + statusIcons.Clear(); if (Character.InPressure) { - statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); + statusIcons.Add(pressureAffliction); } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) { - statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + statusIcons.Add(oxygenLowAffliction); } foreach (Affliction affliction in currentDisplayedAfflictions) { - statusIcons.Add((affliction, affliction.Prefab.Name)); + statusIcons.Add(affliction); } - Vector2 highlightedIconPos = Vector2.Zero; - Rectangle afflictionArea = HUDLayoutSettings.AfflictionAreaLeft; - - // Push the icons down since the portrait doesn't get rendered + int spacing = GUI.IntScale(10); if (Character.ShouldLockHud()) { - afflictionArea.Y += HUDLayoutSettings.PortraitArea.Height; + // Push the icons down since the portrait doesn't get rendered + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.PortraitArea.Height); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing + HUDLayoutSettings.PortraitArea.Height); } + else + { + afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, 0); + hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing); + } + //remove affliction icons for afflictions that no longer exist - bool horizontal = afflictionArea.Width > afflictionArea.Height; - int iconSize = horizontal ? afflictionArea.Height : afflictionArea.Width; - - Point pos = new Point(afflictionArea.Right - iconSize, afflictionArea.Top); + RemoveNonExistentIcons(afflictionIconContainer); + RemoveNonExistentIcons(hiddenAfflictionIconContainer); + void RemoveNonExistentIcons(GUIComponent container) + { + for (int i = container.CountChildren - 1; i >= 0; i--) + { + var child = container.GetChild(i); + if (child.UserData is not AfflictionPrefab afflictionPrefab) { continue; } + if (!statusIcons.Any(s => s.Prefab == afflictionPrefab)) + { + container.RemoveChild(child); + statusIconVisibleTime.Remove(afflictionPrefab); + } + } + } foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.Affliction; + Affliction affliction = statusIcon; AfflictionPrefab afflictionPrefab = affliction.Prefab; - Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); - if (afflictionIconRect.Contains(PlayerInput.MousePosition) && !Character.ShouldLockHud() && GUI.MouseOn == null) + if (!statusIconVisibleTime.ContainsKey(afflictionPrefab)) { statusIconVisibleTime.Add(afflictionPrefab, 0.0f); } + statusIconVisibleTime[afflictionPrefab] += deltaTime; + + var matchingIcon = + afflictionIconContainer.GetChildByUserData(afflictionPrefab) ?? + hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab); + if (matchingIcon == null) { - highlightedAfflictionIcon = statusIcon; - highlightedIconPos = afflictionIconRect.Location.ToVector2(); + matchingIcon = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: null) + { + UserData = afflictionPrefab, + ToolTip = affliction.Prefab.Name, + CanBeSelected = false + }; + if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("PressureHUDWarning"); + } + else if (affliction == pressureAffliction) + { + matchingIcon.ToolTip = TextManager.Get("OxygenHUDWarning"); + } + new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) + { + CanBeFocused = false + }; } - - if (affliction.DamagePerSecond > 1.0f) + if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay) { - Rectangle glowRect = afflictionIconRect; - glowRect.Inflate((int)(20 * GUI.Scale), (int)(20 * GUI.Scale)); - var glow = GUIStyle.GetComponentStyle("OuterGlowCircular"); - glow.Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, glowRect, - GUIStyle.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; } + var image = matchingIcon.GetChild(); + image.Color = GetAfflictionIconColor(afflictionPrefab, affliction); + image.HoverColor = Color.Lerp(image.Color, Color.White, 0.5f); - float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; - - afflictionPrefab.Icon?.Draw(spriteBatch, - pos.ToVector2(), - /*highlightedIcon == statusIcon ? statusIcon.First.Prefab.IconColor : statusIcon.First.Prefab.IconColor * 0.8f,*/ // OLD IMPLEMENTATION - GetAfflictionIconColor(afflictionPrefab, affliction) * alphaMultiplier, - rotate: 0, - scale: iconSize / afflictionPrefab.Icon.size.X); - - if (horizontal) - pos.X -= iconSize + (int)(5 * GUI.Scale); - else - pos.Y += iconSize + (int)(5 * GUI.Scale); + if (affliction.DamagePerSecond > 1.0f && matchingIcon.FlashTimer <= 0.0f) + { + matchingIcon.Flash(useCircularFlash: true, flashDuration: 1.5f, flashRectInflate: Vector2.One * 15.0f * GUI.Scale); + image.Pulsate(Vector2.One, Vector2.One * 1.2f, 1.0f); + } } - if (highlightedAfflictionIcon != null) + afflictionIconContainer.RectTransform.SortChildren((r1, r2) => { - LocalizedString nameTooltip = highlightedAfflictionIcon.Value.NameToolTip; - Vector2 offset = GUIStyle.Font.MeasureString(nameTooltip); + 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; - GUI.DrawString(spriteBatch, - alignment == Alignment.Left ? highlightedIconPos + offset : highlightedIconPos - offset, - nameTooltip, - Color.White * 0.8f, Color.Black * 0.5f); + Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; + foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) + { + hiddenAfflictionHoverArea = Rectangle.Union(hiddenAfflictionHoverArea, child.Rect); } + afflictionIconContainer.Visible = true; + hiddenAfflictionIconContainer.Visible = + showHiddenAfflictionsButton.Rect.Contains(PlayerInput.MousePosition) || + (hiddenAfflictionIconContainer.Visible && hiddenAfflictionHoverArea.Contains(PlayerInput.MousePosition)); + showHiddenAfflictionsButton.Visible = hiddenAfflictionIconContainer.CountChildren > 0; + showHiddenAfflictionsButton.IgnoreLayoutGroups = !showHiddenAfflictionsButton.Visible; + showHiddenAfflictionsButton.Text = $"+{hiddenAfflictionIconContainer.CountChildren}"; + if (Vitality > 0.0f) { float currHealth = healthBar.BarSize; @@ -1126,6 +1198,7 @@ namespace Barotrauma } else { + afflictionIconContainer.Visible = hiddenAfflictionIconContainer.Visible = false; if (Vitality > 0.0f) { float currHealth = healthWindowHealthBar.BarSize; @@ -1150,18 +1223,20 @@ namespace Barotrauma public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { + //use sqrt to make the color change rapidly when strength is low + //(low strength is where seeing the severity of the affliction makes more difference - at high strengths the character is already unconscious or dead) + float colorT = MathF.Sqrt(afflictionStrength / prefab.MaxStrength); // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); + return ToolBox.GradientLerp(colorT, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } - - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); + return ToolBox.GradientLerp(colorT, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); @@ -1172,7 +1247,7 @@ namespace Barotrauma { if (selectedLimb == null) { - afflictionIconContainer.Content.ClearChildren(); + afflictionIconList.Content.ClearChildren(); return; } @@ -1207,7 +1282,7 @@ namespace Barotrauma private void CreateAfflictionInfos(IEnumerable afflictions) { - afflictionIconContainer.ClearChildren(); + afflictionIconList.ClearChildren(); displayedAfflictions.Clear(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault(); @@ -1217,7 +1292,7 @@ namespace Barotrauma { displayedAfflictions.Add((affliction, affliction.Strength)); - var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform), style: "ListBoxElement") + var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconList.Content.RectTransform), style: "ListBoxElement") { UserData = affliction, OnClicked = SelectAffliction @@ -1275,7 +1350,7 @@ namespace Barotrauma } buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); - afflictionIconContainer.RecalculateChildren(); + afflictionIconList.RecalculateChildren(); } private void CreateRecommendedTreatments() @@ -1386,7 +1461,7 @@ namespace Barotrauma recommendedTreatmentContainer.RecalculateChildren(); - afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) => + afflictionIconList.Content.RectTransform.SortChildren((r1, r2) => { var first = r1.GUIComponent.UserData as Affliction; var second = r2.GUIComponent.UserData as Affliction; @@ -1437,7 +1512,10 @@ namespace Barotrauma }; var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), - affliction.Prefab.Description, textAlignment: Alignment.TopLeft, wrap: true) + affliction.Prefab.GetDescription( + affliction.Strength, + Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter), + textAlignment: Alignment.TopLeft, wrap: true) { CanBeFocused = false }; @@ -1449,8 +1527,7 @@ namespace Barotrauma Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f)); - afflictionStrength.Text = strengthTexts[ - MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + afflictionStrength.Text = affliction.GetStrengthText(); Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text); @@ -1482,10 +1559,9 @@ namespace Barotrauma private bool SelectAffliction(GUIButton button, object userData) { bool selected = button.Selected; - foreach (var child in afflictionIconContainer.Content.Children) + foreach (var child in afflictionIconList.Content.Children) { - GUIButton btn = child.GetChild(); - if (btn != null) + if (child is GUIButton btn) { btn.Selected = btn == button && !selected; } @@ -1516,7 +1592,7 @@ namespace Barotrauma afflictionEffectColor = GUIStyle.Green; } - var child = afflictionIconContainer.Content.FindChild(affliction); + var child = afflictionIconList.Content.FindChild(affliction); var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; afflictionStrengthPredictionBar.BarSize = 0.0f; @@ -1581,8 +1657,7 @@ namespace Barotrauma var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; - strengthText.Text = strengthTexts[ - MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + strengthText.Text = affliction.GetStrengthText(); strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); @@ -1836,14 +1911,14 @@ namespace Barotrauma i++; } - if (selectedLimbIndex > -1 && afflictionIconContainer.Content.CountChildren > 0) + if (selectedLimbIndex > -1 && afflictionIconList.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, - new Vector2(afflictionIconContainer.Rect.X, afflictionIconContainer.Rect.Y), + new Vector2(afflictionIconList.Rect.X, afflictionIconList.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 4a8c36df7..bfe5af4d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -600,7 +600,7 @@ namespace Barotrauma { if (damageModifier.DamageMultiplier > 0 && !string.IsNullOrWhiteSpace(damageModifier.DamageParticle)) { - overrideParticle = GameMain.ParticleManager?.FindPrefab(damageModifier.DamageParticle); + overrideParticle = ParticleManager.FindPrefab(damageModifier.DamageParticle); break; } } @@ -647,7 +647,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))), @@ -684,10 +684,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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index bfa8a5194..392d78842 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1136,6 +1136,17 @@ namespace Barotrauma }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("debugdrawlocalization", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !TextManager.DebugDraw; + } + TextManager.DebugDraw = state; + NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White); + }); + AssignRelayToServer("debugdraw", false); + AssignOnExecute("togglevoicechatfilters", (string[] args) => { if (args.None() || !bool.TryParse(args[0], out bool state)) @@ -1695,6 +1706,8 @@ namespace Barotrauma config.Language = language; GameSettings.SetCurrentConfig(config); } + + HashSet missingTexts = new HashSet(); //key = text tag, value = list of languages the tag is missing from Dictionary> missingTags = new Dictionary>(); @@ -1755,20 +1768,38 @@ namespace Barotrauma foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) { - foreach (var property in itemComponentType.GetProperties()) + checkSerializableEntityType(itemComponentType); + } + checkSerializableEntityType(typeof(Item)); + checkSerializableEntityType(typeof(Hull)); + checkSerializableEntityType(typeof(Structure)); + + void checkSerializableEntityType(Type t) + { + foreach (var property in t.GetProperties()) { - if (!property.IsDefined(typeof(InGameEditable), false)) { continue; } + if (!property.IsDefined(typeof(Editable), false)) { continue; } string propertyTag = $"{property.DeclaringType.Name}.{property.Name}"; - addIfMissingAll(language, + if (addIfMissingAll(language, propertyTag.ToIdentifier(), property.Name.ToIdentifier(), - $"sp.{propertyTag}.name".ToIdentifier()); + $"sp.{property.Name}.name".ToIdentifier(), + $"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier()) + { + missingTexts.Add($"{property.Name.FormatCamelCaseWithSpaces()}"); + } - addIfMissingAll(language, + var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description; + + if (addIfMissingAll(language, $"sp.{propertyTag}.description".ToIdentifier(), - $"{property.Name.ToIdentifier()}.description".ToIdentifier()); + $"sp.{property.Name}.description".ToIdentifier(), + $"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier()) + { + missingTexts.Add($"{description}"); + } } } @@ -1792,7 +1823,18 @@ namespace Barotrauma Identifier afflictionId = affliction.TranslationIdentifier; addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language); - addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + + if (affliction.Descriptions.Any()) + { + foreach (var description in affliction.Descriptions) + { + addIfMissing(description.TextTag, language); + } + } + else + { + addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); + } } foreach (var talentTree in TalentTree.JobTalentTrees) @@ -1889,6 +1931,23 @@ namespace Barotrauma ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); SwapLanguage(TextManager.DefaultLanguage); + if (missingTexts.Any()) + { + ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N", + (option) => + { + if (option.ToLowerInvariant() == "y") + { + string path = "newtexts.txt"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + File.WriteAllLines(path, missingTexts); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ToolBox.OpenFileWithShell(Path.GetFullPath(path)); + SwapLanguage(TextManager.DefaultLanguage); + } + }); + } + void addIfMissing(Identifier tag, LanguageIdentifier language) { if (!tags[language].Contains(tag)) @@ -1897,15 +1956,90 @@ namespace Barotrauma missingTags[tag].Add(language); } } - void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) + bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) { if (!potentialTags.Any(t => tags[language].Contains(t))) { var tag = potentialTags.First(); if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } missingTags[tag].Add(language); + return true; + } + return false; + } + })); + + + commands.Add(new Command("checkduplicateloca", "", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("Please specify a file path."); + return; + } + XDocument doc1 = XMLExtensions.TryLoadXml(args[0]); + if (doc1?.Root == null) + { + ThrowError($"Could not load the file \"{args[0]}\""); + return; + } + List<(string tag, string text)> texts = new List<(string tag, string text)>(); + + bool duplicatesFound = false; + foreach (XElement element in doc1.Root.Elements()) + { + string tag = element.Name.ToString(); + string text = element.ElementInnerText(); + if (texts.Any(t => t.tag == tag)) + { + ThrowError($"Duplicate tag \"{tag}\"."); + duplicatesFound = true; } } + if (duplicatesFound) + { + ThrowError($"Aborting, please fix duplicate tags in the file and try again."); + return; + } + + foreach (XElement element in doc1.Root.Elements()) + { + string tag = element.Name.ToString(); + string text = element.ElementInnerText(); + if (texts.Any(t => t.text == text)) + { + if (tag.StartsWith("sp.")) + { + string[] split = tag.Split('.'); + if (split.Length > 3) + { + texts.RemoveAll(t => t.text == text); + string newTag = $"sp.{split[2]}.{split[3]}"; + texts.Add((newTag, text)); + NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\"."); + } + else + { + NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\"."); + } + } + else + { + texts.Add((tag, text)); + ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually."); + } + } + else + { + texts.Add((tag, text)); + } + } + + string filePath = "uniquetexts.xml"; + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; + File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}")); + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => @@ -2585,99 +2719,6 @@ namespace Barotrauma })); #endif - commands.Add(new Command("cleanbuild", "", (string[] args) => - { - /*GameSettings.CurrentConfig.MusicVolume = 0.5f; - GameSettings.CurrentConfig.SoundVolume = 0.5f; - GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true; - GameSettings.CurrentConfig.VoipAttenuationEnabled = true; - NewMessage("Music and sound volume set to 0.5", Color.Green); - - GameSettings.CurrentConfig.GraphicsWidth = 0; - GameSettings.CurrentConfig.GraphicsHeight = 0; - GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed; - NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); - NewMessage("Fullscreen enabled", Color.Green); - - GameSettings.CurrentConfig.VerboseLogging = false; - - if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster") - { - ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!"); - } - - GameSettings.SaveCurrentConfig();*/ - throw new NotImplementedException(); - #warning TODO: reimplement - - var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); - - foreach (string saveFile in saveFiles) - { - Barotrauma.IO.File.Delete(saveFile); - NewMessage("Deleted " + saveFile, Color.Green); - } - - if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) - { - Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); - NewMessage("Deleted temp save folder", Color.Green); - } - - if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath)) - { - var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath); - - foreach (string logFile in logFiles) - { - Barotrauma.IO.File.Delete(logFile); - NewMessage("Deleted " + logFile, Color.Green); - } - } - - if (Barotrauma.IO.File.Exists("filelist.xml")) - { - Barotrauma.IO.File.Delete("filelist.xml"); - NewMessage("Deleted filelist", Color.Green); - } - - if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt")) - { - Barotrauma.IO.File.Delete("Data/bannedplayers.txt"); - NewMessage("Deleted bannedplayers.txt", Color.Green); - } - - if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub")) - { - Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub"); - - NewMessage("Deleted TutorialSub from the submarine folder", Color.Green); - } - - /*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile)) - { - Barotrauma.IO.File.Delete(GameServer.SettingsFile); - NewMessage("Deleted server settings", Color.Green); - } - - if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile)) - { - Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile); - NewMessage("Deleted client permission file", Color.Green); - }*/ - - if (Barotrauma.IO.File.Exists("crashreport.log")) - { - Barotrauma.IO.File.Delete("crashreport.log"); - NewMessage("Deleted crashreport.log", Color.Green); - } - - if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub")) - { - ThrowError("TutorialSub.sub not found!"); - } - })); - commands.Add(new Command("reloadcorepackage", "", (string[] args) => { if (args.Length < 1) @@ -2925,7 +2966,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/EventInput/EventInput.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs index 474f8c570..fdf54af3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/EventInput.cs @@ -70,6 +70,7 @@ namespace EventInput public delegate void CharEnteredHandler(object sender, CharacterEventArgs e); public delegate void KeyEventHandler(object sender, KeyEventArgs e); + public delegate void EditingTextHandler(object sender, TextEditingEventArgs e); public static class EventInput { @@ -88,6 +89,15 @@ namespace EventInput /// public static event KeyEventHandler KeyUp; + +#if !WINDOWS + /// + /// Raised when the user is editing text and IME is in progress. + /// Windows build uses ImeSharp instead because SDL2's IME implementation is broken on Windows (https://github.com/libsdl-org/SDL/issues/2243) + /// + public static event EditingTextHandler EditingText; +#endif + static bool initialized; /// @@ -102,6 +112,9 @@ namespace EventInput } window.TextInput += ReceiveInput; +#if !WINDOWS + window.TextEditing += ReceiveTextEditing; +#endif initialized = true; } @@ -112,6 +125,13 @@ namespace EventInput KeyDown?.Invoke(sender, new KeyEventArgs(e.Key)); } +#if !WINDOWS + private static void ReceiveTextEditing(object sender, TextEditingEventArgs e) + { + EditingText?.Invoke(sender, e); + } +#endif + public static void OnCharEntered(char character) { CharEntered?.Invoke(null, new CharacterEventArgs(character, 0)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs index b52e392a1..f18dbcd73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/EventInput/KeyboardDispatcher.cs @@ -1,8 +1,4 @@ -using System; -using System.Threading; -#if WINDOWS -using System.Windows; -#endif +using Barotrauma; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; @@ -15,6 +11,11 @@ namespace EventInput void ReceiveCommandInput(char command); void ReceiveSpecialInput(Keys key); +#if !WINDOWS + /// Windows build uses ImeSharp instead because SDL2's IME implementation is broken on Windows (https://github.com/libsdl-org/SDL/issues/2243) + void ReceiveEditingInput(string text, int start); +#endif + bool Selected { get; set; } //or Focused } @@ -25,14 +26,26 @@ namespace EventInput EventInput.Initialize(window); EventInput.CharEntered += EventInput_CharEntered; EventInput.KeyDown += EventInput_KeyDown; - } +#if !WINDOWS + EventInput.EditingText += EventInput_TextEditing; +#endif + /* + * SDL by default starts in a state where it accepts IME inputs + * this is bad because this blocks keybinds since the IME thinks + * it's typing in a text box and not forwarding keybinds to the game. + */ + TextInput.StopTextInput(); + } +#if !WINDOWS + public void EventInput_TextEditing(object sender, TextEditingEventArgs e) + { + _subscriber?.ReceiveEditingInput(e.Text, e.Start); + } +#endif public void EventInput_KeyDown(object sender, KeyEventArgs e) { - if (_subscriber == null) - return; - - _subscriber.ReceiveSpecialInput(e.KeyCode); + _subscriber?.ReceiveSpecialInput(e.KeyCode); } void EventInput_CharEntered(object sender, CharacterEventArgs e) @@ -74,12 +87,25 @@ namespace EventInput get { return _subscriber; } set { - if (_subscriber == value) return; - if (_subscriber != null) + if (_subscriber == value) { return; } + + if (_subscriber is GUITextBox) + { + TextInput.StopTextInput(); _subscriber.Selected = false; + } + + if (value is GUITextBox box) + { + TextInput.SetTextInputRect(box.Rect); + TextInput.StartTextInput(); + } + _subscriber = value; if (value != null) + { value.Selected = true; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..ce251c05a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,37 @@ +using Barotrauma.Tutorials; +using Segment = Barotrauma.ObjectiveManager.Segment; + +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public enum CheckType + { + Added, + Completed + } + + [Serialize(CheckType.Completed, IsPropertySaveable.Yes)] + public CheckType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } + + partial void DetermineSuccessProjSpecific(ref bool success) + { + success = false; + if (Identifier.IsEmpty) + { + success = ObjectiveManager.AllActiveObjectivesCompleted(); + } + else if (ObjectiveManager.GetObjective(Identifier) is Segment segment) + { + success = Type switch + { + CheckType.Added => true, + CheckType.Completed => segment.IsCompleted, + _ => false + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 865634dd1..18e93bfbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -323,7 +323,10 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - LocalizedString translatedText = TextManager.ParseInputTypes(TextManager.Get(text)).Fallback(text); + LocalizedString translatedText = speaker?.DisplayName is not null ? + TextManager.GetWithVariable(text, "[speakername]", speaker?.DisplayName) : + TextManager.Get(text); + translatedText = TextManager.ParseInputTypes(translatedText).Fallback(text); if (speaker?.Info != null && drawChathead) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs index b75a1af4a..2cf27ab55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/MessageBoxAction.cs @@ -11,11 +11,13 @@ partial class MessageBoxAction : EventAction if (Type == ActionType.Create || Type == ActionType.ConnectObjective) { CreateMessageBox(); - if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (!ObjectiveTag.IsEmpty) { Identifier id = Identifier.IfEmpty(Text); - var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); - tutorialMode.Tutorial?.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); + var segment = ObjectiveManager.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox); + segment.CanBeCompleted = ObjectiveCanBeCompleted; + segment.ParentId = ParentObjectiveId; + ObjectiveManager.TriggerTutorialSegment(segment, connectObjective: Type == ActionType.ConnectObjective); } } else if (Type == ActionType.Close) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs index ab9b097d3..6420009e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/TutorialSegmentAction.cs @@ -1,50 +1,43 @@ -using Barotrauma.Tutorials; - namespace Barotrauma; partial class TutorialSegmentAction : EventAction { - private Tutorial.Segment segment; + private ObjectiveManager.Segment segment; partial void UpdateProjSpecific() { // Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance) if (Type == SegmentActionType.Trigger) { - segment = Tutorial.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, - new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center), - new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height)); + segment = ObjectiveManager.Segment.CreateInfoBoxSegment(Identifier, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No, + new ObjectiveManager.Segment.Text(TextTag, Width, Height, Anchor.Center), + new ObjectiveManager.Segment.Video(VideoFile, TextTag, Width, Height)); } else if (Type == SegmentActionType.Add) { - segment = Tutorial.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); + segment = ObjectiveManager.Segment.CreateObjectiveSegment(Identifier, !ObjectiveTag.IsEmpty ? ObjectiveTag : Identifier); } - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + if (segment is not null) { - if (tutorialMode.Tutorial is Tutorial tutorial) - { - switch (Type) - { - case SegmentActionType.Trigger: - case SegmentActionType.Add: - tutorial.TriggerTutorialSegment(segment); - break; - case SegmentActionType.Complete: - tutorial.CompleteTutorialSegment(Identifier); - break; - case SegmentActionType.Remove: - tutorial.RemoveTutorialSegment(Identifier); - break; - case SegmentActionType.CompleteAndRemove: - tutorial.CompleteTutorialSegment(Identifier); - tutorial.RemoveTutorialSegment(Identifier); - break; - } - } + segment.CanBeCompleted = CanBeCompleted; + segment.ParentId = ParentObjectiveId; } - else + switch (Type) { - DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!"); + case SegmentActionType.Trigger: + case SegmentActionType.Add: + ObjectiveManager.TriggerTutorialSegment(segment); + break; + case SegmentActionType.Complete: + ObjectiveManager.CompleteTutorialSegment(Identifier); + break; + case SegmentActionType.Remove: + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; + case SegmentActionType.CompleteAndRemove: + ObjectiveManager.CompleteTutorialSegment(Identifier); + ObjectiveManager.RemoveTutorialSegment(Identifier); + break; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 9093450ec..2405fd5db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -651,6 +651,7 @@ namespace Barotrauma break; case NetworkEventType.MISSION: Identifier missionIdentifier = msg.ReadIdentifier(); + int locationIndex = msg.ReadInt32(); string missionName = msg.ReadString(); MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) @@ -660,6 +661,10 @@ namespace Barotrauma { IconColor = prefab.IconColor }; + if (GameMain.GameSession?.Map is { } map && locationIndex > 0 && locationIndex < map.Locations.Count) + { + map.Discover(map.Locations[locationIndex], checkTalents: false); + } } 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/EndMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs new file mode 100644 index 000000000..fdf6fe6cb --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EndMission.cs @@ -0,0 +1,129 @@ +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() + { + 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) + { + EndWaitDuration = 3.0f + }; + } + }, delay: 3.0f); + } + else if (Phase == MissionPhase.AllItemsDestroyed) + { + CoroutineManager.StartCoroutine(wakeUpCoroutine(), name: "EndMission.wakeUpCoroutine"); + } + else if (Phase == MissionPhase.BossKilled) + { + CoroutineManager.Invoke(() => + { + new CameraTransition(boss, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: 3, fadeOut: false, endZoom: 0.1f * GUI.yScale) + { + 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) + { + 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 (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..f49f005b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -43,33 +43,41 @@ namespace Barotrauma List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - LocalizedString name = ""; - - if (reputationReward.Key == "location") + FactionPrefab targetFaction; + if (reputationReward.Key == "location" ) { - name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; + targetFaction = currLocation.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 targetFaction); + } + + LocalizedString name; + if (targetFaction != null) + { + name = $"‖color:{XMLExtensions.ToStringHex(targetFaction.IconColor)}‖{targetFaction.Name}‖end‖"; + } + else + { + name = TextManager.Get(reputationReward.Key); } float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); string formattedValue = ((int)reputationReward.Value).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) 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/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/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 094a4b81c..2456e2ba8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -769,7 +769,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()])); + GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()], radio)); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 85afac3c9..ac2123cc1 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, + Enabled = CanHire(characterInfo), ToolTip = TextManager.GetWithVariable("campaigncrew.givenicknametooltip", "[mouseprimary]", TextManager.Get($"input.{(PlayerInput.MouseButtonsSwapped() ? "rightmouse" : "leftmouse")}")), 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/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 4fcd29809..373fecbbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -357,6 +357,17 @@ namespace Barotrauma string txt = directory; if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } if (!txt.EndsWith("/")) { txt += "/"; } + //get directory info + DirectoryInfo dirInfo = new DirectoryInfo(directory); + try + { + //this will throw an exception if the directory can't be opened + Directory.GetDirectories(directory); + } + catch (UnauthorizedAccessException) + { + continue; + } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { UserData = ItemIsDirectory.Yes diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c158fc21d..85db03a6b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; using Barotrauma.CharacterEditor; using Barotrauma.Extensions; using Barotrauma.Items.Components; @@ -50,6 +49,14 @@ namespace Barotrauma static class GUI { + // Controls where a line is drawn for given coords. + public enum OutlinePosition + { + Default = 0, // Thickness is inside of top left and outside of bottom right coord + Inside = 1, // Thickness is subtracted from the inside + Centered = 2, // Thickness is centered on given coords + Outside = 3, // Tickness is added to the outside + } public static GUICanvas Canvas => GUICanvas.Instance; public static CursorState MouseCursor = CursorState.Default; @@ -133,13 +140,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; } @@ -188,8 +202,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 } }; } } @@ -248,7 +263,17 @@ namespace Barotrauma ScreenChanged = false; } - updateList.ForEach(c => c.DrawAuto(spriteBatch)); + foreach (GUIComponent c in updateList) + { + c.DrawAuto(spriteBatch); + } + + // always draw IME preview on top of everything else + foreach (GUIComponent c in updateList) + { + if (c is not GUITextBox box) { continue; } + box.DrawIMEPreview(spriteBatch); + } if (ScreenOverlayColor.A > 0.0f) { @@ -516,18 +541,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 { @@ -582,6 +606,10 @@ namespace Barotrauma GameMain.Client?.Draw(spriteBatch); + string factionWaterMark = "FACTION/ENDGAME TEST VERSION - please do not publicly share any material you see here!".ToUpper(); + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 100 * yScale) - GUIStyle.SubHeadingFont.MeasureString(factionWaterMark) / 2, factionWaterMark, + GUIStyle.Red * 0.5f, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black * 0.5f, backgroundPadding: 10); + if (Character.Controlled?.Inventory != null) { if (Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead) @@ -667,21 +695,8 @@ namespace Barotrauma } } - public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, float aberrationStrength = 1.0f) + public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color) { - 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); - float scale = Math.Max( (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; @@ -694,10 +709,8 @@ namespace Barotrauma spriteBatch.Draw(backgroundSprite.Texture, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, - null, Color.White, 0.0f, backgroundSprite.size / 2, + null, color, 0.0f, backgroundSprite.size / 2, scale, SpriteEffects.None, 0.0f); - - spriteBatch.End(); } #region Update list @@ -1189,48 +1202,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); @@ -1244,6 +1246,10 @@ namespace Barotrauma UpdateMessages(deltaTime); UpdateSavingIndicator(deltaTime); } + +#if WINDOWS + GUITextBox.UpdateIME(); +#endif } public static void UpdateGUIMessageBoxesOnly(float deltaTime) @@ -1350,7 +1356,7 @@ namespace Barotrauma } } - #region Element drawing +#region Element drawing private static readonly List usedIndicatorAngles = new List(); @@ -1605,6 +1611,54 @@ namespace Barotrauma } } + public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered) + { + Vector2 topLeft = new Vector2(-origin.X, -origin.Y); + Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y); + Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y); + Vector2 actualSize = size; + + switch(outlinePos) + { + case OutlinePosition.Default: + actualSize += new Vector2(thickness); + break; + case OutlinePosition.Centered: + topLeft -= new Vector2(thickness * 0.5f); + topRight -= new Vector2(thickness * 0.5f); + bottomLeft -= new Vector2(thickness * 0.5f); + actualSize += new Vector2(thickness); + break; + case OutlinePosition.Inside: + topRight -= new Vector2(thickness, 0.0f); + bottomLeft -= new Vector2(0.0f, thickness); + break; + case OutlinePosition.Outside: + topLeft -= new Vector2(thickness); + topRight -= new Vector2(0.0f, thickness); + bottomLeft -= new Vector2(thickness, 0.0f); + actualSize += new Vector2(thickness * 2.0f); + break; + } + + Matrix rotate = Matrix.CreateRotationZ(rotation); + topLeft = Vector2.Transform(topLeft, rotate) + position; + topRight = Vector2.Transform(topRight, rotate) + position; + bottomLeft = Vector2.Transform(bottomLeft, rotate) + position; + + Rectangle srcRect = new Rectangle(0, 0, 1, 1); + sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth); + } + + public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f) + { + Rectangle srcRect = new Rectangle(0, 0, 1, 1); + sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth); + } + public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f) { DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth); @@ -1788,9 +1842,9 @@ namespace Barotrauma Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } - #endregion +#endregion - #region Element creation +#region Element creation public static Texture2D CreateCircle(int radius, bool filled = false) { @@ -2162,9 +2216,9 @@ namespace Barotrauma return msgBox; } - #endregion +#endregion - #region Element positioning +#region Element positioning private static List CreateElements(int count, RectTransform parent, Func constructor, Vector2? relativeSize = null, Point? absoluteSize = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, @@ -2363,9 +2417,9 @@ namespace Barotrauma } } - #endregion +#endregion - #region Misc +#region Misc public static void TogglePauseMenu() { if (Screen.Selected == GameMain.MainMenuScreen) { return; } @@ -2603,6 +2657,6 @@ namespace Barotrauma if (!isSavingIndicatorEnabled) { return; } timeUntilSavingIndicatorDisabled = delay; } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 926e5520b..2d8523b58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -9,6 +9,7 @@ using Barotrauma.IO; using RestSharp; using System.Net; using System.Collections.Immutable; +using Barotrauma.Tutorials; namespace Barotrauma { @@ -736,7 +737,7 @@ namespace Barotrauma public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); @@ -759,7 +760,7 @@ namespace Barotrauma public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) { - if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } + if (ObjectiveManager.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index a8ca188cc..465e5d771 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -112,6 +112,10 @@ namespace Barotrauma public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) { } +#endif + public void ReceiveSpecialInput(Keys key) { switch (key) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 71173d75d..fc0b5c098 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -1349,6 +1349,10 @@ namespace Barotrauma public void ReceiveTextInput(string text) { } public void ReceiveCommandInput(char command) { } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) { } +#endif + public void ReceiveSpecialInput(Keys key) { switch (key) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 3aa1cff1c..f14c4e1c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -24,7 +24,8 @@ namespace Barotrauma InGame, Vote, Hint, - Tutorial + Tutorial, + Warning // Keep this last so that it's always drawn in front } private bool IsAnimated => type == Type.InGame || type == Type.Hint || type == Type.Tutorial; @@ -84,8 +85,8 @@ namespace Barotrauma public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); - public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null) - : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize) + public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null, Type type = Type.Default) + : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type) { this.Buttons[0].OnClicked = Close; } @@ -147,7 +148,7 @@ namespace Barotrauma Tag = tag.ToIdentifier(); #warning TODO: These should be broken into separate methods at least - if (type == Type.Default || type == Type.Vote) + if (type == Type.Default || type == Type.Vote || type == Type.Warning) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; @@ -266,7 +267,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/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 99461d746..168f53a7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Barotrauma @@ -285,8 +286,8 @@ namespace Barotrauma /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. /// - public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) + public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null, + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { if (color.HasValue) @@ -551,6 +552,8 @@ namespace Barotrauma if (TextGetter != null) { Text = TextGetter(); } + string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); + Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; if (overflowClipActive) { @@ -561,7 +564,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - if (!text.IsNullOrEmpty()) + if (!string.IsNullOrEmpty(textToShow)) { Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset; if (RoundToNearestPixel) @@ -570,7 +573,8 @@ namespace Barotrauma pos.Y = (int)pos.Y; } - Color currentTextColor = State == ComponentState.Hover || State == ComponentState.HoverSelected ? HoverTextColor : TextColor; + Color currentTextColor = State is ComponentState.Hover or ComponentState.HoverSelected ? HoverTextColor : TextColor; + if (!enabled) { currentTextColor = disabledTextColor; @@ -582,8 +586,14 @@ namespace Barotrauma if (!HasColorHighlight) { - string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); + if (TextManager.DebugDraw) + { + if (!text.NestedStr.Loaded || text.NestedStr.Language == LanguageIdentifier.None) + { + colorToShow = Color.Magenta; + } + } if (Shadow) { @@ -597,10 +607,10 @@ namespace Barotrauma { if (OverrideRichTextDataAlpha) { - RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + RichTextData?.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); } - Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase); + Font.DrawStringWithColors(spriteBatch, textToShow, pos, + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData, alignment: textAlignment, forceUpperCase: ForceUpperCase); } Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 27a558c5c..dae60140f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -11,7 +11,7 @@ namespace Barotrauma public delegate void TextBoxEvent(GUITextBox sender, Keys key); - public class GUITextBox : GUIComponent, IKeyboardSubscriber + public partial class GUITextBox : GUIComponent, IKeyboardSubscriber { public event TextBoxEvent OnSelected; public event TextBoxEvent OnDeselected; @@ -67,12 +67,12 @@ namespace Barotrauma private int selectionEndIndex; private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex; - private GUICustomComponent caretAndSelectionRenderer; + private readonly GUICustomComponent caretAndSelectionRenderer; private bool mouseHeldInside; private readonly Memento memento = new Memento(); - + // Skip one update cycle, fixes Enter key instantly deselecting the chatbox private bool skipUpdate; @@ -189,6 +189,7 @@ namespace Barotrauma base.Font = value; if (textBlock == null) { return; } textBlock.Font = value; + imePreviewTextHandler.Font = Font; } } @@ -253,6 +254,8 @@ namespace Barotrauma public override bool PlaySoundOnSelect { get; set; } = true; + private readonly IMEPreviewTextHandler imePreviewTextHandler; + public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) @@ -264,6 +267,7 @@ namespace Barotrauma frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap); + imePreviewTextHandler = new IMEPreviewTextHandler(textBlock.Font); GUIStyle.Apply(textBlock, "", this); if (font != null) { textBlock.Font = font; } CaretEnabled = true; @@ -295,18 +299,17 @@ namespace Barotrauma textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } Font = textBlock.Font; - Enabled = true; - rectT.SizeChanged += () => + rectT.SizeChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; rectT.ScaleChanged += () => { if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } - caretPosDirty = true; + caretPosDirty = true; }; } @@ -381,14 +384,16 @@ namespace Barotrauma { GUI.KeyboardDispatcher.Subscriber = null; } + OnDeselected?.Invoke(this, Keys.None); + imePreviewTextHandler.Reset(); } public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectOffset = null) { frame.Flash(color, flashDuration, useRectangleFlash, useCircularFlash, flashRectOffset); } - + protected override void Update(float deltaTime) { if (!Visible) return; @@ -673,6 +678,18 @@ namespace Barotrauma break; } } +#if !WINDOWS + public void ReceiveEditingInput(string text, int start) + { + if (string.IsNullOrEmpty(text)) + { + if (start is 0) { imePreviewTextHandler.Reset(); } + return; + } + + imePreviewTextHandler.UpdateText(text, start); + } +#endif public void ReceiveSpecialInput(Keys key) { @@ -864,6 +881,24 @@ namespace Barotrauma } } + public void DrawIMEPreview(SpriteBatch spriteBatch) + { + if (!imePreviewTextHandler.HasText) { return; } + + Vector2 imePosition = CaretScreenPos; + int inflate = GUI.IntScale(3); + + RectangleF rect = new RectangleF(imePosition, imePreviewTextHandler.TextSize); + rect.Inflate(inflate, inflate); + + RectangleF borderRect = rect; + borderRect.Inflate(1, 1); + + GUI.DrawFilledRectangle(spriteBatch, borderRect, Color.White, depth: 0.02f); + GUI.DrawFilledRectangle(spriteBatch, rect, Color.Black, depth: 0.01f); + Font.DrawString(spriteBatch, imePreviewTextHandler.PreviewText, imePosition, GUIStyle.Orange, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0, alignment: textBlock.TextAlignment, forceUpperCase: textBlock.ForceUpperCase); + } + private void CalculateSelection() { string textDrawn = Censor ? textBlock.CensoredText : WrappedText; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs new file mode 100644 index 000000000..ca20e30cf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBoxIME.cs @@ -0,0 +1,86 @@ +using ImeSharp; +using Microsoft.Xna.Framework; +using System; + +namespace Barotrauma; + +/// +/// A class for handling Input Method Editor (used for inputting e.g. Chinese and Japanese text) +/// +public partial class GUITextBox : GUIComponent +{ + private static bool initialized; + + public static GUIFrame IMEWindow { get; set; } + public static GUITextBlock IMETextBlock { get; set; } + + public static void UpdateIME() + { + if (!initialized) { InitializeIME(); } + if (GUI.KeyboardDispatcher.Subscriber is GUITextBox { Selected: true }) + { + IMEWindow?.AddToGUIUpdateList(order: 10); + } + } + + private static void InitializeIME() + { + InputMethod.Initialize(GameMain.Instance.Window.Hwnd, false); + InputMethod.TextCompositionCallback = OnTextComposition; + InputMethod.CommitTextCompositionCallback = OnCommitTextComposition; + InputMethod.Enabled = true; + IMEWindow = new GUIFrame(new RectTransform(new Point(GUI.IntScale(300), GUI.IntScale(300)), GUI.Canvas), "InnerFrame") { CanBeFocused = false, Visible = false }; + IMETextBlock = new GUITextBlock(new RectTransform(Vector2.One, IMEWindow.RectTransform), "") { CanBeFocused = false }; + + initialized = true; + } + + private static void OnTextComposition(IMEString compositionText, int cursorPosition, IMEString[] candidateList, int candidatePageStart, int candidatePageSize, int candidateSelection) + { + if (GUI.KeyboardDispatcher.Subscriber is not GUITextBox { Selected: true } textBox) { return; } + IMEWindow.Visible = true; + string text = compositionText.ToString().Insert(cursorPosition, "|"); + if (candidateList != null) + { + text += "\n"; + for (int i = 0; i < candidatePageSize; i++) + { + string candidateStr = $"\t{candidatePageStart + i + 1} {candidateList[i]}"; + if (candidateSelection == i) + { + candidateStr = $" ‖color:{XMLExtensions.ToStringHex(Color.White)}‖{candidateStr}‖end‖"; + } + candidateStr += "\n"; + text += candidateStr; + } + } + IMETextBlock.Text = RichString.Rich(text); + + IMEWindow.RectTransform.NonScaledSize = new Point( + Math.Max(IMEWindow.Rect.Width, (int)IMETextBlock.TextSize.X + GUI.IntScale(32)), + (int)IMETextBlock.TextSize.Y); + + Point windowPos = new Point(textBox.Rect.X, textBox.Rect.Bottom); + if (windowPos.Y + IMEWindow.Rect.Height > GameMain.GraphicsHeight) + { + windowPos.Y = textBox.Rect.Y - IMEWindow.Rect.Height; + } + IMEWindow.RectTransform.ScreenSpaceOffset = windowPos; + } + + private static void OnCommitTextComposition(string text) + { + if (IMEWindow.Visible) + { + foreach (char c in text) + { + if (!char.IsControl(c)) + { + GUI.KeyboardDispatcher.Subscriber?.ReceiveTextInput(c); + } + } + } + IMEWindow.Visible = false; + } +} + diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index ded2b870e..dec3485bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -65,7 +65,7 @@ namespace Barotrauma get; private set; } - public static Rectangle AfflictionAreaLeft + public static Rectangle HealthBarAfflictionArea { get; private set; } @@ -143,7 +143,7 @@ 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); - AfflictionAreaLeft = 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; @@ -173,7 +173,7 @@ namespace Barotrauma int objectiveListAreaX = HealthWindowAreaLeft.Right + Padding; int objectiveListAreaY = ButtonAreaTop.Bottom + Padding; - TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (AfflictionAreaLeft.Top - Padding) - objectiveListAreaY); + TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (HealthBarAfflictionArea.Top - Padding) - objectiveListAreaY); int votingAreaWidth = (int)(400 * GUI.Scale); int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth; @@ -193,7 +193,7 @@ namespace Barotrauma DrawRectangle(CrewArea, Color.Blue * 0.5f); DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f); DrawRectangle(HealthBarArea, Color.Red * 0.5f); - DrawRectangle(AfflictionAreaLeft, 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs new file mode 100644 index 000000000..96f137a08 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/IMEPreviewText.cs @@ -0,0 +1,54 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public sealed class IMEPreviewTextHandler + { + public string PreviewText { get; private set; } = string.Empty; + public Vector2 TextSize { get; private set; } + public bool HasText => !string.IsNullOrEmpty(PreviewText); + + // This has to be settable because for some reason we update the font of GUITextBox in some places + public GUIFont Font { get; set; } + + public IMEPreviewTextHandler(GUIFont font) + { + Font = font; + } + + public void Reset() + { + TextSize = Vector2.Zero; + PreviewText = string.Empty; + } + + public void UpdateText(string text, int start) + { + if (string.IsNullOrEmpty(text) && start is 0) + { + Reset(); + return; + } + + int totalLength = start + text.Length; + string newText = PreviewText; + if (newText.Length > totalLength) + { + newText = newText[..totalLength]; + } + + if (totalLength > newText.Length) + { + // this is required for some reason on Windows + // my guess is that the order which TextEditing events come thru is not guaranteed + newText = newText.PadRight(totalLength); + } + + newText = newText.Remove(start, text.Length).Insert(start, text); + PreviewText = newText; + TextSize = Font.MeasureString(PreviewText); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index b8026a67c..349c8ecdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -117,7 +117,7 @@ namespace Barotrauma 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 = TextureLoader.FromFile("Content/UI/MainMenuVignette.png"); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; SetSelectedTip(TextManager.Get("LoadingScreenTip")); @@ -138,13 +138,6 @@ 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; @@ -165,7 +158,7 @@ namespace Barotrauma 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); + spriteBatch.Draw(overlay, Vector2.Zero, null, Color.White, 0.0f, Vector2.Zero, Math.Min(GameMain.GraphicsWidth / (float)overlay.Width, GameMain.GraphicsHeight / (float)overlay.Height), SpriteEffects.None, 0.0f); float noiseStrength = (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0); float noiseScale = (float)PerlinNoise.CalculatePerlin(noiseT * 5.0f, noiseT * 2.0f, 0) * 4.0f; @@ -174,10 +167,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 +205,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 +226,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 +237,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 +251,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 +282,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 +310,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 +319,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 +429,7 @@ 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))?.Texture; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 54373bcff..6f5272743 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -12,7 +12,7 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement; namespace Barotrauma { [SuppressMessage("ReSharper", "UnusedVariable")] - internal class MedicalClinicUI + internal sealed class MedicalClinicUI { private enum ElementState { @@ -127,12 +127,14 @@ namespace Barotrauma { public readonly GUIComponent Panel; public readonly GUIListBox HealList; + public readonly GUIComponent TreatAllButton; public readonly List HealElements; - public CrewHealList(GUIListBox healList, GUIComponent panel) + public CrewHealList(GUIListBox healList, GUIComponent panel, GUIComponent treatAllButton) { Panel = panel; HealList = healList; + TreatAllButton = treatAllButton; HealElements = new List(); } } @@ -179,7 +181,7 @@ namespace Barotrauma private PopupAfflictionList? selectedCrewAfflictionList; private bool isWaitingForServer; private const float refreshTimerMax = 3f; - private float refreshTimer = 0; + private float refreshTimer; private PlayerBalanceElement? playerBalanceElement; @@ -196,7 +198,7 @@ namespace Barotrauma { new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") { - OnClicked = (_, __) => + OnClicked = (_, _) => { parent.ClearChildren(); CreateUI(); @@ -254,7 +256,7 @@ namespace Barotrauma continue; } - CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty()); + CreatePendingHealElement(healList.HealList.Content, crewMember, healList, ImmutableArray.Empty); } // check if there are elements that the crew doesn't have @@ -309,7 +311,7 @@ namespace Barotrauma private void UpdateCrewPanel() { - if (!(crewHealList is { } healList)) { return; } + if (crewHealList is not { } healList) { return; } ImmutableArray crew = MedicalClinic.GetCrewCharacters(); @@ -334,12 +336,21 @@ namespace Barotrauma healList.HealList.Content.RemoveChild(element.UIElement); } - IEnumerable orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100); + IEnumerable orderedList = healList.HealElements.OrderBy(static element => element.Target.Character?.HealthPercentage ?? 100); foreach (CrewElement element in orderedList) { element.UIElement.SetAsLastChild(); } + + healList.TreatAllButton.Enabled = false; + foreach (CrewElement element in healList.HealElements) + { + if (element.Afflictions.Count is 0) { continue; } + + healList.TreatAllButton.Enabled = true; + break; + } } private static void UpdateAfflictionList(CrewElement healElement) @@ -350,7 +361,7 @@ namespace Barotrauma // sum up all the afflictions and their strengths Dictionary afflictionAndStrength = new Dictionary(); - foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a))) + foreach (Affliction affliction in health.GetAllAfflictions().Where(MedicalClinic.IsHealable)) { if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) { @@ -446,8 +457,8 @@ namespace Barotrauma }; GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); - GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); + new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); + new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); @@ -480,22 +491,24 @@ namespace Barotrauma Stretch = true }; - // GUILayoutGroup sortLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), clinicContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - - // new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), sortLayout.RectTransform), TextManager.Get("campaignstore.sortby"), font: GUI.SubHeadingFont); - - // GUIDropDown sortDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1f), sortLayout.RectTransform)); - // - // foreach (SortMode mode in Enum.GetValues(typeof(SortMode)).Cast()) - // { - // sortDropdown.AddItem(TextManager.Get($"medicalclinic.sortmode.{mode}"), mode); - // } - // - // sortDropdown.SelectItem(SortMode.Severity); - GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, clinicContainer.RectTransform)); - crewHealList = new CrewHealList(crewList, parent); + GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) + { + OnClicked = (_, _) => + { + isWaitingForServer = true; + medicalClinic.TreatAllButtonAction(OnReceived); + return true; + } + }; + + crewHealList = new CrewHealList(crewList, parent, treatAllButton); + + void OnReceived(MedicalClinic.CallbackOnlyRequest obj) + { + isWaitingForServer = false; + } } private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) @@ -525,9 +538,9 @@ namespace Barotrauma TextColor = GUIStyle.Red }; - MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; + MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember(info); - crewBackground.OnClicked = (_, __) => + crewBackground.OnClicked = (_, _) => { SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top)); return true; @@ -618,7 +631,7 @@ namespace Barotrauma pendingHealList = list; } - private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions) + private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, ImmutableArray afflictions) { CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); if (healInfo is null) { return; } @@ -803,7 +816,7 @@ namespace Barotrauma } allComponents.Add(treatAllButton); - treatAllButton.OnClicked = (_, __) => + treatAllButton.OnClicked = (_, _) => { ImmutableArray afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); if (!afflictions.Any()) { return true; } @@ -845,9 +858,9 @@ namespace Barotrauma GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); - Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f); + Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); - LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + LocalizedString vitalityText = affliction.VitalityDecrease == 0 ? string.Empty : TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.VitalityDecrease).ToString()); GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) { TextColor = textColor, @@ -856,7 +869,7 @@ namespace Barotrauma AutoScaleHorizontal = true }; - LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + LocalizedString severityText = Affliction.GetStrengthText(affliction.Strength, affliction.Prefab.MaxStrength); GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextColor = textColor, @@ -873,9 +886,13 @@ namespace Barotrauma { RelativeSpacing = 0.05f }; - GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true) + LocalizedString description = affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.OtherCharacter); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), + description, + font: GUIStyle.SmallFont, + wrap: true) { - ToolTip = prefab.Description + ToolTip = description }; bool truncated = false; while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n')) @@ -919,10 +936,9 @@ namespace Barotrauma } else { - MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember + MedicalClinic.NetCrewMember newMember = crewMember with { - CharacterInfoID = crewMember.CharacterInfoID, - Afflictions = Array.Empty() + Afflictions = ImmutableArray.Empty }; existingMember = newMember; @@ -936,7 +952,7 @@ namespace Barotrauma } } - existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray(); + existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); ToggleElements(ElementState.Disabled, elementsToDisable); medicalClinic.AddPendingButtonAction(existingMember, request => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 4f87042dd..554846cde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -724,7 +724,7 @@ namespace Barotrauma ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - CurrentLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); + newLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); }); } } @@ -745,7 +745,7 @@ namespace Barotrauma } ?? Enumerable.Empty(); foreach (var button in itemCategoryButtons) { - if (!(button.UserData is MapEntityCategory category)) + if (button.UserData is not MapEntityCategory category) { continue; } @@ -852,6 +852,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() @@ -892,8 +914,9 @@ namespace Barotrauma void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) { - if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo)) + if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy()) { + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : @@ -918,7 +941,8 @@ namespace Barotrauma SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } - SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); + + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue); existingItemFrames.Add(itemFrame); } } @@ -1110,7 +1134,7 @@ namespace Barotrauma private void SetPriceGetters(GUIComponent itemFrame, bool buying) { - if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + if (itemFrame == null || itemFrame.UserData is not PurchasedItem pi) { return; } if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) { @@ -1302,6 +1326,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); @@ -1330,6 +1356,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; } @@ -1354,6 +1382,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; } @@ -1376,10 +1406,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 @@ -1409,6 +1441,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)) @@ -1667,8 +1712,8 @@ namespace Barotrauma { foreach (var subItem in subItems) { - if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } - if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } + if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; } + if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } AddOwnedItem(subItem); } @@ -1701,7 +1746,7 @@ namespace Barotrauma void AddOwnedItem(Item item) { - if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; } + if (item?.Prefab.GetPriceInfo(ActiveStore) is not PriceInfo priceInfo) { return; } bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { @@ -1729,14 +1774,14 @@ namespace Barotrauma private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled) { - if (!(itemFrame?.UserData is PurchasedItem pi)) { return; } + if (itemFrame?.UserData is not PurchasedItem pi) { return; } bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled; if (!refreshFrameStatus) { return; } if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon) { 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) { @@ -1841,11 +1886,7 @@ namespace Barotrauma LocalizedString toolTip = string.Empty; if (purchasedItem.ItemPrefab != null) { - toolTip = purchasedItem.ItemPrefab.Name; - if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty()) - { - toolTip += $"\n{purchasedItem.ItemPrefab.Description}"; - } + toolTip = purchasedItem.ItemPrefab.GetTooltip(); if (itemQuantity != null) { if (itemQuantity.AllNonEmpty) @@ -1858,8 +1899,25 @@ 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 = toolTip; + itemComponent.ToolTip = RichString.Rich(toolTip); } if (ownedLabel != null) { @@ -1995,11 +2053,23 @@ namespace Barotrauma int totalPrice = 0; foreach (var item in itemsToPurchase) { - if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) + if (item is null) { continue; } + + if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) { itemsToRemove.Add(item); continue; } + + if (item.ItemPrefab.DefaultPrice.RequiresUnlock) + { + if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab)) + { + itemsToRemove.Add(item); + continue; + } + } + totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); @@ -2169,7 +2239,7 @@ namespace Barotrauma { needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count || itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || - itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + itemsToSellFromSub.Any(i => prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is not PurchasedItem prev || i.Quantity != prev.Quantity) || prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 7a9a97fc5..326768d32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -406,7 +406,7 @@ 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 @@ -472,7 +472,7 @@ namespace Barotrauma if (transferService) { subsToShow.AddRange(GameMain.GameSession.OwnedSubmarines); - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + subsToShow.Sort(ComparePrice); string currentSubName = CurrentOrPendingSubmarine().Name; int currentIndex = subsToShow.FindIndex(s => s.Name == currentSubName); if (currentIndex != -1) @@ -484,7 +484,11 @@ namespace Barotrauma { subsToShow.AddRange((GameMain.Client is null ? SubmarineInfo.SavedSubmarines : MultiPlayerCampaign.GetCampaignSubs()) .Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); + if (GameMain.GameSession.Campaign?.Map?.CurrentLocation is Location currentLocation) + { + subsToShow.RemoveAll(sub => !currentLocation.IsSubmarineAvailable(sub)); + } + subsToShow.Sort(ComparePrice); } if (transferService) @@ -492,10 +496,14 @@ namespace Barotrauma SetConfirmButtonState(selectedSubmarine != null && selectedSubmarine.Name != CurrentOrPendingSubmarine().Name); } - subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); pageCount = Math.Max(1, (int)Math.Ceiling(subsToShow.Count / (float)submarinesPerPage)); UpdatePaging(); ContentRefreshRequired = false; + + static int ComparePrice(SubmarineInfo x, SubmarineInfo y) + { + return x.Price.CompareTo(y.Price) * 100 + x.Name.CompareTo(y.Name); + } } private SubmarineInfo GetSubToDisplay(int index) @@ -673,7 +681,7 @@ namespace Barotrauma { if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) { - return Submarine.MainSub.Info; + return Submarine.MainSub?.Info; } else { @@ -789,7 +797,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), @@ -802,7 +812,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(); @@ -860,7 +870,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 295818c7b..8b88bc72c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -34,7 +34,7 @@ namespace Barotrauma private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; - private GUIFrame pendingChangesFrame = null; + public static GUIFrame PendingChangesFrame = null; public static Color OwnCharacterBGColor = Color.Gold * 0.7f; private bool isTransferMenuOpen; @@ -44,6 +44,7 @@ namespace Barotrauma private float transferMenuOpenState; private bool transferMenuStateCompleted; private readonly HashSet registeredEvents = new HashSet(); + private readonly TalentMenu talentMenu = new TalentMenu(); private class LinkedGUI { @@ -206,15 +207,8 @@ namespace Barotrauma transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); - if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null) - { - int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); - talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; - if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) - { - talentApplyButton.Flash(GUIStyle.Orange); - } - } + + talentMenu?.Update(); if (SelectedTab != InfoFrameTab.Crew) { return; } if (linkedGUIList == null) { return; } @@ -325,11 +319,11 @@ namespace Barotrauma AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) }, style: null); - pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); + PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false) { - NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame); } SetBalanceText(balanceText, campaignMode.Bank.Balance); @@ -403,7 +397,7 @@ namespace Barotrauma CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; case InfoFrameTab.Talents: - CreateCharacterInfo(infoFrameHolder); + talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character); break; } } @@ -957,16 +951,26 @@ namespace Barotrauma if (character != null) { - if (GameMain.Client == null) + if (GameMain.Client is null) { GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); } else { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); + GameMain.Client.SelectCrewCharacter(character, preview); if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); } } + + if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton) + { + talentButton.OnClicked = (button, o) => + { + talentMenu.CreateGUI(infoFrameHolder, character); + return true; + }; + } } else if (client != null) { @@ -1494,27 +1498,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; @@ -1526,6 +1512,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); @@ -1537,6 +1547,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) { @@ -1774,373 +1785,10 @@ namespace Barotrauma sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); } } - private Color unselectedColor = new Color(240, 255, 255, 225); - private Color unselectableColor = new Color(100, 100, 100, 225); - private Color pressedColor = new Color(60, 60, 60, 225); - - private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>(); - private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); - private List selectedTalents = new List(); - - private GUITextBlock experienceText; - private GUIProgressBar experienceBar; - private GUITextBlock talentPointText; - private GUIListBox skillListBox; - - private GUIButton talentApplyButton, - talentResetButton; private GUIImage talentPointNotification; - private readonly ImmutableDictionary talentStageStyles = new Dictionary - { - { TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") }, - { TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") }, - { TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") }, - }.ToImmutableDictionary(); - - private readonly ImmutableDictionary talentStageBackgroundColors = new Dictionary - { - { TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) }, - { TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) }, - { TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) }, - { TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) }, - { TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) }, - }.ToImmutableDictionary(); - - private void CreateCharacterInfo(GUIFrame infoFrame) - { - infoFrame.ClearChildren(); - talentButtons.Clear(); - talentCornerIcons.Clear(); - - GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); - int padding = GUI.IntScale(15); - GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); - - GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null); - - GUIFrame characterSettingsFrame = null; - GUILayoutGroup characterLayout = null; - if (!(GameMain.NetworkMember is null)) - { - characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false }; - characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); - GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); - GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); - GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); - } - - Character controlledCharacter = Character.Controlled; - CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo; - if (info == null) { return; } - - Job job = info.Job; - - GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) - { - AbsoluteSpacing = GUI.IntScale(10), - Stretch = true - }; - - GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true); - - new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => - { - float posY = component.Rect.Center.Y - component.Rect.Width / 2; - info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); - }); - - GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) - { - AbsoluteSpacing = GUI.IntScale(5), - CanBeFocused = true - }; - - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); - - if (!info.OmitJobInMenus) - { - nameBlock.TextColor = job.Prefab.UIColor; - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; - } - - if (info.PersonalityTrait != null) - { - LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName); - Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); - GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); - traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - } - - IEnumerable talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); - if (talentsOutsideTree.Count() > 0) - { - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null); - - GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); - - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont); - talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); - - var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) - { - AutoHideScrollBar = false, - ResizeContentToMakeSpaceForScrollBar = false - }; - extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); - extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65)); - extraTalentLayout.Recalculate(); - extraTalentList.ForceLayoutRecalculation(); - - foreach (var extraTalent in talentsOutsideTree) - { - var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true) - { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description), - Color = GUIStyle.Green - }; - img.RectTransform.SizeChanged += () => - { - img.RectTransform.MaxSize = new Point(img.Rect.Height); - }; - } - } - - GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight) - { - AbsoluteSpacing = GUI.IntScale(5), - Stretch = true - }; - - GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont); - - skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); - CreateSkillList(controlledCharacter, info, skillListBox); - - new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); - - GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); - - if (controlledCharacter == null) - { - talentTreeListBox.Enabled = false; - } - else - { - if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - selectedTalents = info.GetUnlockedTalentsInTree().ToList(); - - List subTreeNames = new List(); - foreach (var subTree in talentTree.TalentSubTrees) - { - GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); - GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter); - - GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - int elementPadding = GUI.IntScale(8); - Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; - GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); - - for (int i = 0; i < 4; i++) - { - GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null); - - Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; - - GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground") - { - Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] - }; - GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; - - GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) - { - CanBeFocused = false, - Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked] - }; - - Point iconSize = cornerIcon.RectTransform.NonScaledSize; - cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); - - if (subTree.TalentOptionStages.Length <= i) { continue; } - - TalentOption talentOption = subTree.TalentOptionStages[i]; - GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); - GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - - foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t)) - { - if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; } - GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) - { - CanBeFocused = false - }; - - GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); - GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) - { - ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description), - UserData = talent.Identifier, - PressedColor = pressedColor, - Enabled = controlledCharacter != null, - OnClicked = (button, userData) => - { - // deselect other buttons in tier by removing their selected talents from pool - foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) - { - if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button) - { - if (!controlledCharacter.HasTalent(otherTalentIdentifier)) - { - selectedTalents.Remove(otherTalentIdentifier); - } - } - } - Identifier talentIdentifier = (Identifier)userData; - - if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) - { - if (!selectedTalents.Contains(talentIdentifier)) - { - selectedTalents.Add(talentIdentifier); - } - } - else if (!controlledCharacter.HasTalent(talentIdentifier)) - { - selectedTalents.Remove(talentIdentifier); - } - - UpdateTalentInfo(); - return true; - }, - }; - - talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; - - GUIComponent iconImage; - if (talent.Icon is null) - { - iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) - { - OutlineColor = GUIStyle.Red, - TextColor = GUIStyle.Red, - PressedColor = unselectableColor, - DisabledColor = unselectableColor, - CanBeFocused = false, - }; - } - else - { - iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) - { - PressedColor = unselectableColor, - DisabledColor = unselectableColor * 0.5f, - CanBeFocused = false, - }; - } - iconImage.Enabled = talentButton.Enabled; - talentButtons.Add((talentButton, iconImage)); - } - talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight)); - } - } - GUITextBlock.AutoScaleAndNormalize(subTreeNames); - - GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true) - { - RelativeSpacing = 0.01f, - Stretch = true - }; - - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform)); - GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); - - experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), - barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) - { - IsHorizontal = true, - }; - - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) - { - Shadow = true, - ToolTip = TextManager.Get("experiencetooltip") - }; - - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; - - talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") - { - OnClicked = ResetTalentSelection - }; - talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") - { - OnClicked = ApplyTalentSelection, - }; - GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); - } - - if (!(GameMain.NetworkMember is null)) - { - GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), - text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = false - }; - newCharacterBox.TextBlock.AutoScaleHorizontal = true; - - newCharacterBox.OnClicked = (button, o) => - { - if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) - { - GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => - { - newCharacterBox.Text = TextManager.Get("settings"); - if (pendingChangesFrame != null) - { - NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); - } - OpenMenu(); - }); - return true; - } - - OpenMenu(); - return true; - - void OpenMenu() - { - characterSettingsFrame!.Visible = true; - content.Visible = false; - } - }; - - if (!(characterLayout is null)) - { - GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); - new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? - { - OnClicked = (button, o) => - { - GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); - characterSettingsFrame!.Visible = false; - content.Visible = true; - return true; - } - }; - } - } - - UpdateTalentInfo(); - } - - private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) + public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent) { parent.Content.ClearChildren(); List skillNames = new List(); @@ -2154,10 +1802,10 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight); - float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level; + float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level); if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { - int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level)); string stringColor = skillChange switch { > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), @@ -2168,123 +1816,17 @@ namespace Barotrauma RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } - //skillContainer.Recalculate(); + skillContainer.Recalculate(); } parent.RecalculateChildren(); GUITextBlock.AutoScaleAndNormalize(skillNames); } - private void UpdateTalentInfo() - { - Character controlledCharacter = Character.Controlled; - if (controlledCharacter?.Info == null) { return; } - - if (SelectedTab != InfoFrameTab.Talents) { return; } - - bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents(); - - if (unlockedAllTalents) - { - experienceText.Text = string.Empty; - experienceBar.BarSize = 1f; - } - else - { - experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}"; - experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel(); - } - - selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - - string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString(); - - int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count(); - - if (unlockedAllTalents) - { - talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖"); - } - else if (talentCount > 0) - { - string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; - LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); - talentPointText.SetRichText(localizedString); - } - else - { - talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft)); - } - - foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons) - { - TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents); - GUIComponentStyle newStyle = talentStageStyles[state]; - icon.ApplyStyle(newStyle); - icon.Color = newStyle.Color; - frame.Color = talentStageBackgroundColors[state]; - glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted; - } - - foreach (var talentButton in talentButtons) - { - Identifier talentIdentifier = (Identifier)talentButton.button.UserData; - bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); - Color newTalentColor = unselectable ? unselectableColor : unselectedColor; - Color hoverColor = Color.White; - - if (controlledCharacter.HasTalent(talentIdentifier)) - { - newTalentColor = GUIStyle.Green; - } - else if (selectedTalents.Contains(talentIdentifier)) - { - newTalentColor = GUIStyle.Orange; - hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f); - } - - talentButton.icon.Color = newTalentColor; - talentButton.icon.HoverColor = hoverColor; - } - - CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox); - } - - private void ApplyTalents(Character controlledCharacter) - { - selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - foreach (Identifier talent in selectedTalents) - { - controlledCharacter.GiveTalent(talent); - if (GameMain.Client != null) - { - GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); - } - } - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentInfo(); - } - - private bool ApplyTalentSelection(GUIButton guiButton, object userData) - { - Character controlledCharacter = Character.Controlled; - ApplyTalents(controlledCharacter); - return true; - } - - private bool ResetTalentSelection(GUIButton guiButton, object userData) - { - Character controlledCharacter = Character.Controlled; - if (controlledCharacter?.Info == null) { return false; } - selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); - UpdateTalentInfo(); - return true; - } - public void OnExperienceChanged(Character character) { if (character != Character.Controlled) { return; } - UpdateTalentInfo(); + talentMenu.UpdateTalentInfo(); } public void OnClose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs new file mode 100644 index 000000000..b1314b27f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -0,0 +1,805 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using static Barotrauma.TalentTree; +using static Barotrauma.TalentTree.TalentStages; + +namespace Barotrauma +{ + internal readonly record struct TalentShowCaseButton(ImmutableHashSet Buttons, + GUIComponent IconComponent); + + internal readonly record struct TalentButton(GUIComponent IconComponent, + TalentPrefab Prefab) + { + public Identifier Identifier => Prefab.Identifier; + } + + internal readonly record struct TalentCornerIcon(Identifier TalentTree, + int Index, + GUIImage IconComponent, + GUIFrame BackgroundComponent, + GUIFrame GlowComponent); + + internal readonly struct TalentTreeStyle + { + public readonly GUIComponentStyle ComponentStyle; + public readonly Color Color; + + public TalentTreeStyle(string componentStyle, Color color) + { + ComponentStyle = GUIStyle.GetComponentStyle(componentStyle); + Color = color; + } + } + + internal sealed class TalentMenu + { + public const string ManageBotTalentsButtonUserData = "managebottalentsbutton"; + + private Character? character; + private CharacterInfo? characterInfo; + + private static readonly Color unselectedColor = new Color(240, 255, 255, 225), + unselectableColor = new Color(100, 100, 100, 225), + pressedColor = new Color(60, 60, 60, 225), + lockedColor = new Color(48, 48, 48, 255), + unlockedColor = new Color(24, 37, 31, 255), + availableColor = new Color(50, 47, 33, 255); + + private static readonly ImmutableDictionary talentStageStyles = + new Dictionary + { + [Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor), + [Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor), + [Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor), + [Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor), + [Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor) + }.ToImmutableDictionary(); + + private readonly HashSet talentButtons = new HashSet(); + private readonly HashSet talentShowCaseButtons = new HashSet(); + private readonly HashSet showCaseTalentFrames = new HashSet(); + private readonly HashSet talentCornerIcons = new HashSet(); + private HashSet selectedTalents = new HashSet(); + + private readonly Queue showCaseClosureQueue = new(); + + private GUIListBox? skillListBox; + private GUITextBlock? talentPointText; + private GUIProgressBar? experienceBar; + private GUITextBlock? experienceText; + private GUILayoutGroup? skillLayout; + + private GUIButton? talentApplyButton, + talentResetButton; + + public void CreateGUI(GUIFrame parent, Character? targetCharacter) + { + parent.ClearChildren(); + talentButtons.Clear(); + talentShowCaseButtons.Clear(); + talentCornerIcons.Clear(); + showCaseTalentFrames.Clear(); + + character = targetCharacter; + characterInfo = targetCharacter?.Info; + + GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = GUI.IntScale(15); + GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null); + + GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null); + + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = GUI.IntScale(10), + Stretch = true + }; + + if (characterInfo is null) { return; } + + CreateStatPanel(contentLayout, characterInfo); + + new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine"); + + if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree)) + { + CreateTalentMenu(contentLayout, characterInfo, talentTree!); + } + + CreateFooter(contentLayout, characterInfo); + UpdateTalentInfo(); + + if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo)) + { + CreateMultiplayerCharacterSettings(frame, content); + } + } + + private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content) + { + if (skillLayout is null) { return; } + + GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false }; + GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); + GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); + GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); + GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); + + GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight), + text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall") + { + IgnoreLayoutGroups = false, + TextBlock = + { + AutoScaleHorizontal = true + } + }; + + newCharacterBox.OnClicked = (button, o) => + { + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + if (TabMenu.PendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame); + } + + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + content.Visible = false; + } + }; + + GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter); + new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? + { + OnClicked = (button, o) => + { + GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName); + characterSettingsFrame.Visible = false; + content.Visible = true; + return true; + } + }; + } + + private void CreateStatPanel(GUIComponent parent, CharacterInfo info) + { + Job job = info.Job; + + GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true); + + new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) => + { + info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false); + }); + + GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform)) + { + AbsoluteSpacing = GUI.IntScale(5), + CanBeFocused = true + }; + + GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont); + + if (!info.OmitJobInMenus) + { + nameBlock.TextColor = job.Prefab.UIColor; + GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor }; + } + + if (info.PersonalityTrait != null) + { + LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName); + Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); + GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont); + traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); + } + + ImmutableHashSet talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet(); + if (talentsOutsideTree.Any()) + { + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null); + + GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter); + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont) + { + AutoScaleVertical = true + }; + talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y); + + var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true) + { + AutoHideScrollBar = false, + ResizeContentToMakeSpaceForScrollBar = false + }; + extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter); + extraTalentLayout.Recalculate(); + extraTalentList.ForceLayoutRecalculation(); + + foreach (var extraTalent in talentsOutsideTree) + { + if (extraTalent is null) { continue; } + GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true) + { + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)), + Color = GUIStyle.Green + }; + } + } + + skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight) + { + AbsoluteSpacing = GUI.IntScale(5), + Stretch = true + }; + + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont); + + skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null); + TabMenu.CreateSkillList(info.Character, info, skillListBox); + } + + private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree) + { + GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter)); + + selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet(); + + List subTreeNames = new List(); + foreach (var subTree in tree.TalentSubTrees) + { + GUIListBox talentList; + GUIComponent talentParent; + Vector2 treeSize; + switch (subTree.Type) + { + case TalentTreeType.Primary: + talentList = mainList; + treeSize = new Vector2(1f, 0.5f); + break; + case TalentTreeType.Specialization: + talentList = GetSpecializationList(); + treeSize = new Vector2(0.333f, 1f); + break; + default: + throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\""); + } + talentParent = talentList.Content; + + GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter) + { + Stretch = true + }; + + if (subTree.Type != TalentTreeType.Primary) + { + GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter) + { MinSize = new Point(0, GUI.IntScale(30)) }, style: null); + subtreeTitleFrame.RectTransform.IsFixedSize = true; + int elementPadding = GUI.IntScale(8); + Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; + GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); + } + + int optionAmount = subTree.TalentOptionStages.Length; + for (int i = 0; i < optionAmount; i++) + { + TalentOption option = subTree.TalentOptionStages[i]; + CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info); + } + subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width, + subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing))); + subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height); + subTreeLayoutGroup.Recalculate(); + + if (subTree.Type == TalentTreeType.Specialization) + { + talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height))); + talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height); + } + } + + var specializationList = GetSpecializationList(); + GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height)); + + GUITextBlock.AutoScaleAndNormalize(subTreeNames); + + GUIListBox GetSpecializationList() + { + if (mainList.Content.Children.LastOrDefault() is GUIListBox specList) + { + return specList; + } + + GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + return newSpecializationList; + } + } + + private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info) + { + int elementPadding = GUI.IntScale(8); + GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter) + { MinSize = new Point(0, GUI.IntScale(65)) }, style: null); + + Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize; + + GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), + style: "TalentBackground") + { + Color = talentStageStyles[Locked].Color + }; + GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; + + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) + { + CanBeFocused = false, + Color = talentStageStyles[Locked].Color + }; + + Point iconSize = cornerIcon.RectTransform.NonScaledSize; + cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2); + + GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); + GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + + HashSet talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet(); + HashSet buttonsToAdd = new(); + + Dictionary> showCaseTalentParents = new(); + Dictionary showCaseTalentButtonsToAdd = new(); + + foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents) + { + talentOptionIdentifiers.Add(showCaseTalentIdentifier); + Point parentSize = talentBackground.RectTransform.NonScaledSize; + GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip") + { + UserData = showCaseTalentIdentifier, + IgnoreLayoutGroups = true, + Visible = false + }; + GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft); + GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true }; + showCaseTalentParents.Add(showcaseLayout, talents); + showCaseTalentFrames.Add(showCaseFrame); + } + + foreach (Identifier talentId in talentOptionIdentifiers) + { + if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; } + + bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId); + GUIComponent talentParent = talentOptionLayoutGroup; + + foreach (var (key, value) in showCaseTalentParents) + { + if (value.Contains(talentId)) + { + talentParent = key; + break; + } + } + + GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null) + { + CanBeFocused = false + }; + + GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); + GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) + { + ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)), + UserData = talent.Identifier, + PressedColor = pressedColor, + Enabled = info.Character != null, + OnClicked = (button, userData) => + { + if (isShowCaseTalent) + { + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId) + { + component.RectTransform.ScreenSpaceOffset = new Point((int)(button.Rect.Location.X - component.Rect.Width / 2f + button.Rect.Width / 2f), button.Rect.Location.Y - component.Rect.Height); + component.Visible = true; + } + else + { + component.Visible = false; + } + } + + return true; + } + + if (character is null) { return false; } + + if (talentOption.MaxChosenTalents is 1) + { + // deselect other buttons in tier by removing their selected talents from pool + foreach (Identifier identifier in selectedTalents) + { + if (character.HasTalent(identifier) || identifier == talentId) { continue; } + + if (talentOptionIdentifiers.Contains(identifier)) + { + selectedTalents.Remove(identifier); + } + } + } + + Identifier talentIdentifier = (Identifier)userData; + + if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents)) + { + if (!selectedTalents.Contains(talentIdentifier)) + { + selectedTalents.Add(talentIdentifier); + } + else + { + selectedTalents.Remove(talentIdentifier); + } + } + else if (!character.HasTalent(talentIdentifier)) + { + selectedTalents.Remove(talentIdentifier); + } + + UpdateTalentInfo(); + return true; + }, + }; + + talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent; + + GUIComponent iconImage; + if (talent.Icon is null) + { + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) + { + OutlineColor = GUIStyle.Red, + TextColor = GUIStyle.Red, + PressedColor = unselectableColor, + DisabledColor = unselectableColor, + CanBeFocused = false, + }; + } + else + { + iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true) + { + Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White, + PressedColor = unselectableColor, + DisabledColor = unselectableColor * 0.5f, + CanBeFocused = false, + }; + } + + iconImage.Enabled = talentButton.Enabled; + if (isShowCaseTalent) + { + showCaseTalentButtonsToAdd.Add(talentId, iconImage); + continue; + } + + buttonsToAdd.Add(new TalentButton(iconImage, talent)); + } + + foreach (TalentButton button in buttonsToAdd) + { + talentButtons.Add(button); + } + + foreach (var (key, value) in showCaseTalentButtonsToAdd) + { + HashSet buttons = new(); + foreach (Identifier identifier in talentOption.ShowCaseTalents[key]) + { + if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) { continue; } + + buttons.Add(button); + } + + talentShowCaseButtons.Add(new TalentShowCaseButton(buttons.ToImmutableHashSet(), value)); + } + + talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight)); + } + + private void CreateFooter(GUIComponent parent, CharacterInfo info) + { + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true) + { + RelativeSpacing = 0.01f, + Stretch = true + }; + + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform)); + GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); + + experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), + barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) + { + IsHorizontal = true, + }; + + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight) + { + Shadow = true, + ToolTip = TextManager.Get("experiencetooltip") + }; + + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) + { AutoScaleVertical = true }; + + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") + { + OnClicked = ApplyTalentSelection, + }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); + } + + private bool ResetTalentSelection(GUIButton guiButton, object userData) + { + if (characterInfo is null) { return false; } + selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet(); + UpdateTalentInfo(); + return true; + } + + private void ApplyTalents(Character controlledCharacter) + { + foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents)) + { + controlledCharacter.GiveTalent(talent); + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); + } + } + + UpdateTalentInfo(); + } + + private bool ApplyTalentSelection(GUIButton guiButton, object userData) + { + if (character is null) { return false; } + + ApplyTalents(character); + return true; + } + + public void UpdateTalentInfo() + { + if (character is null || characterInfo is null) { return; } + + bool unlockedAllTalents = character.HasUnlockedAllTalents(); + + if (experienceBar is null || experienceText is null) { return; } + + if (unlockedAllTalents) + { + experienceText.Text = string.Empty; + experienceBar.BarSize = 1f; + } + else + { + experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}"; + experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel(); + } + + selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet(); + + string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString(); + + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); + + if (unlockedAllTalents) + { + talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖"); + } + else if (talentCount > 0) + { + string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; + LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); + talentPointText?.SetRichText(localizedString); + } + else + { + talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft)); + } + + foreach (TalentCornerIcon cornerIcon in talentCornerIcons) + { + TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents); + TalentTreeStyle style = talentStageStyles[state]; + GUIComponentStyle newStyle = style.ComponentStyle; + cornerIcon.IconComponent.ApplyStyle(newStyle); + cornerIcon.IconComponent.Color = newStyle.Color; + cornerIcon.BackgroundComponent.Color = style.Color; + cornerIcon.GlowComponent.Visible = state == Highlighted; + } + + foreach (TalentButton talentButton in talentButtons) + { + TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents); + ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride); + } + + foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons) + { + TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents); + ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option.None()); + } + + if (skillListBox is null) { return; } + + TabMenu.CreateSkillList(character, characterInfo, skillListBox); + + static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) + { + bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier); + TalentStages stage = unselectable ? Locked : Available; + if (unselectable) + { + stage = Locked; + } + + if (character.HasTalent(talentIdentifier)) + { + stage = Unlocked; + } + else if (selectedTalents.Contains(talentIdentifier)) + { + stage = Highlighted; + } + + return stage; + } + + static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option colorOverride) + { + Color color = stage switch + { + Invalid => unselectableColor, + Locked => unselectableColor, + Unlocked => GetColorOrOverride(GUIStyle.Green, colorOverride), + Highlighted => GetColorOrOverride(GUIStyle.Orange, colorOverride), + Available => GetColorOrOverride(unselectedColor, colorOverride), + _ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null) + }; + + component.Color = color; + component.HoverColor = Color.Lerp(color, Color.White, 0.7f); + + static Color GetColorOrOverride(Color color, Option colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color; + } + + // this could also be reused for setting colors for talentCornerIcons but that's for another time + static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection buttons, IReadOnlyCollection selectedTalents) + { + HashSet talentStages = new HashSet(); + foreach (TalentButton button in buttons) + { + talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents)); + } + + TalentStages collectiveStage = talentStages.All(static stage => stage is Locked) + ? Locked + : Available; + + foreach (TalentStages stage in talentStages) + { + if (stage is Highlighted) + { + collectiveStage = Highlighted; + break; + } + + if (stage is Unlocked) + { + collectiveStage = Unlocked; + break; + } + } + + return collectiveStage; + } + } + + public void Update() + { + if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; } + + int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count(); + talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) + { + talentApplyButton.Flash(GUIStyle.Orange); + } + + while (showCaseClosureQueue.TryDequeue(out Identifier identifier)) + { + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier) + { + component.Visible = false; + } + } + } + + bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0; + bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit(); + + foreach (GUIComponent component in showCaseTalentFrames) + { + if (component.UserData is not Identifier identifier) { continue; } + + component.AddToGUIUpdateList(order: 1); + if (!component.Visible) { continue; } + + if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition))) + { + showCaseClosureQueue.Enqueue(identifier); + } + } + } + + private static bool IsOwnCharacter(CharacterInfo? info) + { + if (info is null) { return false; } + + CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo; + if (ownCharacterInfo is null) { return false; } + + return info == ownCharacterInfo; + } + + public static bool CanManageTalents(CharacterInfo targetInfo) + { + // in singleplayer we can do whatever we want + if (GameMain.IsSingleplayer) { return true; } + + // always allow managing talents for own character + if (IsOwnCharacter(targetInfo)) { return true; } + + // don't allow controlling non-bot characters + if (targetInfo.Character is not { IsBot: true }) { return false; } + + // lastly check if we have the permission to do this + return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 0a78a8663..c23fa8c18 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.Globalization; using System.Linq; @@ -78,6 +79,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. @@ -93,6 +96,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) { @@ -121,6 +125,7 @@ namespace Barotrauma private void RefreshAll() { + characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both); switch (selectedUpgradeTab) { case UpgradeTab.Repairs: @@ -278,10 +283,13 @@ namespace Barotrauma * | upgrades | maintenance | <- 1/3rd empty space | * |---------------------------------------------------------------------------------------------------| */ - GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; + GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.4f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true); GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true); - new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + var header = new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); + header.RectTransform.MaxSize = new Point((int)(header.TextSize.X + header.Padding.X + header.Padding.Z), int.MaxValue); + 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 }; @@ -433,8 +441,8 @@ namespace Barotrauma Location location = Campaign.Map.CurrentLocation; - int hullRepairCost = Campaign.GetHullRepairCost(); - int itemRepairCost = Campaign.GetItemRepairCost(); + int hullRepairCost = CampaignMode.GetHullRepairCost(); + int itemRepairCost = CampaignMode.GetItemRepairCost(); int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost; if (location != null) { @@ -1080,7 +1088,7 @@ namespace Barotrauma public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { - int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + 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)); } @@ -1222,7 +1230,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) @@ -1639,7 +1647,7 @@ namespace Barotrauma GUITextBlock priceLabel = textBlocks[0]; 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 (priceLabel != null && !WaitForServerUpdate) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 6afb4c50f..4db7544b1 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,14 +178,14 @@ 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: @@ -218,6 +219,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 +227,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,7 +235,7 @@ 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())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index e8069b34b..a0a55d717 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -30,7 +30,7 @@ namespace Barotrauma Data = data, OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) => { - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); + GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/"); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 226487ec1..21dc20279 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -4,6 +4,7 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Steam; using Barotrauma.Transition; +using Barotrauma.Tutorials; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -774,9 +775,9 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning) { - tutorialMode.Tutorial.CloseActiveContentGUI(); + ObjectiveManager.CloseActiveContentGUI(); } else if (GameSession.IsTabMenuOpen) { @@ -791,6 +792,10 @@ namespace Barotrauma { GUI.TogglePauseMenu(); } + else if (GameSession?.Campaign is { ShowCampaignUI: true, ForceMapUI: false }) + { + GameSession.Campaign.ShowCampaignUI = false; + } //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC else if ((Character.Controlled == null || !itemHudActive()) && CharacterHealth.OpenHealthWindow == null @@ -824,7 +829,7 @@ namespace Barotrauma Paused = (DebugConsole.IsOpen || DebugConsole.Paused || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || - (GameSession?.GameMode is TutorialMode tutoMode && tutoMode.Tutorial.ContentRunning)) && + (GameSession?.GameMode is TutorialMode && ObjectiveManager.ContentRunning)) && (NetworkMember == null || !NetworkMember.GameStarted); if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { @@ -858,8 +863,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) + else if (ObjectiveManager.ContentRunning && GameSession?.GameMode is TutorialMode tutorialMode) { + ObjectiveManager.VideoPlayer.Update(); tutorialMode.Update((float)Timing.Step); } else @@ -1200,7 +1206,7 @@ namespace Barotrauma base.OnExiting(sender, args); } - public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) + public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null) { if (string.IsNullOrEmpty(url)) { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } @@ -1218,7 +1224,14 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (btn, userdata) => { - ToolBox.OpenFileWithShell(url); + try + { + ToolBox.OpenFileWithShell(url); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to open the url {url}", e); + } msgBox.Close(); return true; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 5820ca4cd..0969b5574 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 { 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/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 19a52394e..500250be8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -1,9 +1,9 @@ using Barotrauma.Extensions; -using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,8 +15,6 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected LocalizedString overlayText, overlayTextBottom; - protected Color overlayTextColor; protected Sprite overlaySprite; private TransitionType prevCampaignUIAutoOpenType; @@ -29,6 +27,12 @@ namespace Barotrauma protected GUIFrame campaignUIContainer; public CampaignUI CampaignUI; + public SlideshowPlayer SlideshowPlayer + { + get; + protected set; + } + public static CancellationTokenSource StartRoundCancellationToken { get; private set; } public bool ForceMapUI @@ -76,6 +80,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) @@ -86,6 +91,8 @@ namespace Barotrauma } } + private static bool IsOwner(Client client) => client != null && client.IsOwner; + /// /// There is a server-side implementation of the method in /// @@ -97,10 +104,8 @@ namespace Barotrauma return GameMain.Client.HasPermission(permissions) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || - //allow managing if no-one with permissions is alive - GameMain.Client.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (c.IsOwner || c.HasPermission(permissions))); + AnyOneAllowedToManageCampaign(permissions); } public static bool AllowedToManageWallets() @@ -127,32 +132,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; @@ -192,6 +175,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]"); @@ -203,6 +187,10 @@ namespace Barotrauma } break; } + if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) + { + endRoundButton.Visible = false; + } if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } @@ -266,7 +254,7 @@ namespace Barotrauma Rand.ThreadId = Thread.CurrentThread.ManagedThreadId; try { - GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror); + GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); } catch (Exception e) { @@ -283,6 +271,18 @@ namespace Barotrauma return loadTask; } + protected SubmarineInfo GetPredefinedStartOutpost() + { + if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty()) + { + return new SubmarineInfo(parameters.OutpostFilePath.Value) + { + OutpostGenerationParams = parameters + }; + } + return null; + } + partial void NPCInteractProjSpecific(Character npc, Character interactor) { if (npc == null || interactor == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 94ba1b79a..5d4a84fe1 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); @@ -834,8 +792,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++) @@ -844,11 +800,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); @@ -861,6 +812,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)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index dc7bc12b1..a5964a235 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,10 @@ namespace Barotrauma /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { - CampaignMetadata = new CampaignMetadata(this); + CampaignMetadata = new CampaignMetadata(); UpgradeManager = new UpgradeManager(this); Settings = settings; + InitFactions(); map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { @@ -89,7 +98,6 @@ namespace Barotrauma CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } - InitCampaignData(); InitUI(); } @@ -100,6 +108,19 @@ namespace Barotrauma { IsFirstRound = false; + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "metadata": + CampaignMetadata = new CampaignMetadata(subElement); + break; + } + } + + CampaignMetadata ??= new CampaignMetadata(); + InitFactions(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -114,9 +135,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 +154,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,8 +280,7 @@ namespace Barotrauma private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { - GameMain.GameSession.StartRound(level, - mirrorLevel: mirror); + GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition"); @@ -296,34 +310,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 +320,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 +338,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; @@ -407,6 +391,11 @@ namespace Barotrauma GUI.SetSavingIndicatorState(success); crewDead = false; + if (success) + { + // Event history must be registered before ending the round or it will be cleared + GameMain.GameSession.EventManager.RegisterEventHistory(); + } GameMain.GameSession.EndRound("", traitorResults, transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; @@ -437,60 +426,65 @@ namespace Barotrauma case TransitionType.ProgressToNextEmptyLocation: TotalPassedLevels++; break; + case TransitionType.End: + EndCampaign(); + IsFirstRound = true; + break; } - Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(this, transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); - var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, + GUI.ClearMessages(); + + //-------------------------------------- + if (transitionType != TransitionType.End) + { + var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, fadeOut: false, panDuration: EndTransitionDuration); - 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) - { - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - GameMain.GameSession.EventManager.RegisterEventHistory(); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - else - { - PendingSubmarineSwitch = null; - EnableRoundSummaryGameOverState(); - } - - 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; @@ -498,7 +492,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; @@ -507,39 +504,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() || @@ -590,11 +562,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); @@ -604,11 +584,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/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index a9c7f14e8..d6c8f225d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,7 +1,5 @@ using Barotrauma.Extensions; -using Barotrauma.IO; using Barotrauma.Items.Components; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -25,107 +23,9 @@ namespace Barotrauma.Tutorials #region Tutorial variables public readonly Identifier Identifier; - public LocalizedString DisplayName { get; } - - public bool ContentRunning { get; private set; } - - private GUIComponent infoBox; - private Action infoBoxClosedCallback; - - private VideoPlayer videoPlayer; - private Point screenResolution; - private WindowMode windowMode; - private float prevUIScale; - - private GUILayoutGroup objectiveGroup; - private readonly LocalizedString objectiveTextTranslated; - - private readonly List ActiveObjectives = new List(); - private const float ObjectiveComponentAnimationTime = 1.5f; - private Segment ActiveContentSegment { get; set; } - - public class Segment - { - public readonly record struct Text( - Identifier Tag, - int Width = DefaultWidth, - int Height = DefaultHeight, - Anchor Anchor = Anchor.Center); - - public readonly record struct Video( - string FullPath, - Identifier TextTag, - int Width = DefaultWidth, - int Height = DefaultHeight) - { - public string FileName => Path.GetFileName(FullPath.CleanUpPath()); - public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); - } - - private const int DefaultWidth = 450; - private const int DefaultHeight = 80; - - public GUIImage ObjectiveStateIndicator; - public GUIButton ObjectiveButton; - public GUITextBlock LinkedTextBlock; - public LocalizedString ObjectiveText; - - public readonly Identifier Id; - public readonly Text TextContent; - public readonly Video VideoContent; - public readonly AutoPlayVideo AutoPlayVideo; - - public Action OnClickObjective; - - public TutorialSegmentType SegmentType { get; private set; } - - public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); - } - - public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - return new Segment(id, objectiveTextTag, onClickObjective); - } - - public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) - { - return new Segment(id, objectiveTextTag); - } - - private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - AutoPlayVideo = autoPlayVideo; - TextContent = textContent; - VideoContent = videoContent; - SegmentType = TutorialSegmentType.InfoBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - OnClickObjective = onClickObjective; - SegmentType = TutorialSegmentType.MessageBox; - } - - private Segment(Identifier id, Identifier objectiveTextTag) - { - Id = id; - ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); - SegmentType = TutorialSegmentType.Objective; - } - - public void ConnectMessageBox(Segment messageBoxSegment) - { - SegmentType = TutorialSegmentType.MessageBox; - OnClickObjective = messageBoxSegment.OnClickObjective; - } - } + public LocalizedString Description { get; } + private bool completed; public bool Completed @@ -163,6 +63,8 @@ namespace Barotrauma.Tutorials public readonly List<(Entity entity, Identifier iconStyle)> Icons = new List<(Entity entity, Identifier iconStyle)>(); + public bool Paused { get; private set; } + #endregion #region Tutorial Controls @@ -171,8 +73,7 @@ namespace Barotrauma.Tutorials { Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier(); DisplayName = TextManager.Get(Identifier); - objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); - + Description = TextManager.Get($"tutorial.{prefab.Identifier}.description"); TutorialPrefab = prefab; eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); } @@ -260,35 +161,26 @@ namespace Barotrauma.Tutorials tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); - Initialize(); + GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; + GameMain.GameSession.CrewManager.AutoHideCrewList(); + + if (Character.Controlled?.Inventory is CharacterInventory inventory) + { + foreach (Item item in inventory.AllItemsMod) + { + if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } + item.Unequip(Character.Controlled); + Character.Controlled.Inventory.RemoveItem(item); + } + } yield return CoroutineStatus.Success; } - private void Initialize() - { - GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; - GameMain.GameSession.CrewManager.AutoHideCrewList(); - - if (Character.Controlled is Character character) - { - foreach (Item item in character.Inventory.AllItemsMod) - { - if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; } - item.Unequip(character); - character.Inventory.RemoveItem(item); - } - } - } - public void Start() { - videoPlayer = new VideoPlayer(); GameMain.Instance.ShowLoading(Loading()); - ActiveObjectives.Clear(); - ActiveContentSegment = null; - - CreateObjectiveFrame(); + ObjectiveManager.ResetObjectives(); // Setup doors: Clear all requirements, unless the door is setup as locked. foreach (var item in Item.ItemList) @@ -304,24 +196,8 @@ namespace Barotrauma.Tutorials } } - public void AddToGUIUpdateList() - { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) - { - CreateObjectiveFrame(); - } - if (ActiveObjectives.Count > 0) - { - objectiveGroup?.AddToGUIUpdateList(order: -1); - } - infoBox?.AddToGUIUpdateList(order: 100); - videoPlayer?.AddToGUIUpdateList(order: 100); - } - public void Update() { - videoPlayer?.Update(); - if (character != null) { if (character.Oxygen < 1) @@ -342,8 +218,7 @@ namespace Barotrauma.Tutorials { GUI.PreventPauseMenuToggle = false; } - ContentRunning = false; - infoBox = null; + ObjectiveManager.ClearContent(); } else { @@ -374,18 +249,6 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } - public void CloseActiveContentGUI() - { - if (videoPlayer.IsPlaying) - { - videoPlayer.Stop(); - } - else if (infoBox != null) - { - CloseInfoFrame(); - } - } - public IEnumerable UpdateState() { while (GameMain.Instance.LoadingScreenOpen || Level.Loaded == null || Level.Loaded.Generating) @@ -432,13 +295,56 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(WaitBeforeFade); + Action onEnd = () => GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + + TutorialPrefab nextTutorialPrefab = null; + bool displayEndMessage = + TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Restart || + (TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Continue && TutorialPrefab.Prefabs.TryGet(TutorialPrefab.EndMessage.NextTutorialIdentifier, out nextTutorialPrefab)); + + if (displayEndMessage) + { + Paused = true; + var endingMessageBox = new GUIMessageBox( + headerText: "", + text: TextManager.Get($"{Identifier}.completed"), + buttons: new LocalizedString[] + { + TextManager.Get(nextTutorialPrefab is null ? "restart" : "campaigncontinue"), + TextManager.Get("pausemenuquit") + }); + + endingMessageBox.Buttons[0].OnClicked += (_, _) => + { + if (nextTutorialPrefab is null) + { + onEnd = () => Restart(null, null); + } + else + { + onEnd = () => + { + GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + new Tutorial(nextTutorialPrefab).Start(); + }; + } + return true; + }; + endingMessageBox.Buttons[0].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[0].OnClicked += (_, _) => Paused = false; + endingMessageBox.Buttons[1].OnClicked += endingMessageBox.Close; + endingMessageBox.Buttons[1].OnClicked += (_, _) => Paused = false; + } + + while (Paused) { yield return CoroutineStatus.Running; } + var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: FadeOutTime); Completed = true; while (endCinematic.Running) { yield return CoroutineStatus.Running; } Stop(); - GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + onEnd(); } } @@ -450,379 +356,15 @@ namespace Barotrauma.Tutorials return true; } - public void TriggerTutorialSegment(Segment segment, bool connectObjective = false) - { - if (segment.SegmentType != TutorialSegmentType.InfoBox) - { - ActiveObjectives.Add(segment); - AddToObjectiveList(segment, connectObjective); - return; - } - - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - - var title = TextManager.Get(segment.Id); - LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); - tutorialText = TextManager.ParseInputTypes(tutorialText); - - switch (segment.AutoPlayVideo) - { - case AutoPlayVideo.Yes: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: LoadActiveContentVideo); - break; - case AutoPlayVideo.No: - infoBox = CreateInfoFrame( - title, - tutorialText, - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: StopCurrentContentSegment, - onVideoButtonClicked: LoadActiveContentVideo); - break; - } - } - - public void CompleteTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) - { - //return if already completed - if (segment.ObjectiveStateIndicator.Style == style) { return; } - segment.ObjectiveStateIndicator.ApplyStyle(style); - } - segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:{segmentId}:Completed"); - } - - public void RemoveTutorialSegment(Identifier segmentId) - { - if (GetActiveObjective(segmentId) is not Segment segment) - { - DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!"); - return; - } - segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); - segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); - var parent = segment.LinkedTextBlock.Parent; - parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => - { - ActiveObjectives.Remove(segment); - objectiveGroup?.Recalculate(); - }); - parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); - segment.ObjectiveButton.OnClicked = null; - segment.ObjectiveButton.CanBeFocused = false; - } - - private Segment GetActiveObjective(Identifier id) => ActiveObjectives.FirstOrDefault(s => s.Id == id); - public void Stop() { if (tutorialCoroutine != null) { CoroutineManager.StopCoroutines(tutorialCoroutine); } - ContentRunning = false; - infoBox = null; - videoPlayer?.Remove(); + ObjectiveManager.ResetUI(); } #endregion - - #region Objectives - - /// - /// Create the objective list that holds the objectives (called on start and on resolution change) - /// - private void CreateObjectiveFrame() - { - var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null); - objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) - { - AbsoluteSpacing = (int)GUIStyle.Font.LineHeight - }; - for (int i = 0; i < ActiveObjectives.Count; i++) - { - AddToObjectiveList(ActiveObjectives[i]); - } - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; - prevUIScale = GUI.Scale; - } - - /// - /// Stops content running and adds the active segment to the objective list - /// - private void StopCurrentContentSegment() - { - if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) - { - ActiveObjectives.Add(ActiveContentSegment); - AddToObjectiveList(ActiveContentSegment); - } - ContentRunning = false; - ActiveContentSegment = null; - } - - /// - /// Adds the segment to the objective list - /// - private void AddToObjectiveList(Segment segment, bool connectExisting = false) - { - if (connectExisting) - { - if (ActiveObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) - { - existingSegment.ConnectMessageBox(segment); - SetButtonBehavior(existingSegment); - } - return; - } - - var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) - { - AbsoluteOffset = GetObjectiveHiddenPosition(), - MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) - }; - var frame = new GUIFrame(frameRt, style: null) - { - CanBeFocused = true - }; - objectiveGroup.Recalculate(); - - segment.LinkedTextBlock = new GUITextBlock( - new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), - TextManager.ParseInputTypes(segment.ObjectiveText), - wrap: true); - - var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); - segment.LinkedTextBlock.RectTransform.NonScaledSize = size; - segment.LinkedTextBlock.RectTransform.MinSize = size; - segment.LinkedTextBlock.RectTransform.MaxSize = size; - segment.LinkedTextBlock.RectTransform.IsFixedSize = true; - frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); - frame.RectTransform.IsFixedSize = true; - - var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); - segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); - - SetTransparent(segment.LinkedTextBlock); - - segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) - { - ToolTip = objectiveTextTranslated - }; - SetButtonBehavior(segment); - SetTransparent(segment.ObjectiveButton); - - frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); - - static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; - - void SetButtonBehavior(Segment segment) - { - segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; - segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => - { - if (segment.SegmentType == TutorialSegmentType.InfoBox) - { - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - ReplaySegmentVideo(segment); - } - else - { - ShowSegmentText(segment); - } - } - else if (segment.SegmentType == TutorialSegmentType.MessageBox) - { - segment.OnClickObjective?.Invoke(); - } - return true; - }; - } - } - - private void ReplaySegmentVideo(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - LoadVideo(segment); - } - - private void ShowSegmentText(Segment segment) - { - if (ContentRunning) { return; } - Inventory.DraggingItems.Clear(); - ContentRunning = true; - ActiveContentSegment = segment; - infoBox = CreateInfoFrame( - TextManager.Get(segment.Id), - TextManager.Get(segment.TextContent.Tag), - segment.TextContent.Width, - segment.TextContent.Height, - segment.TextContent.Anchor, - hasButton: true, - onInfoBoxClosed: () => ContentRunning = false, - onVideoButtonClicked: () => LoadVideo(segment)); - } - - private Point GetObjectiveHiddenPosition(RectTransform rt = null) - { - return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); - } - - #endregion - - #region InfoFrame - - private void CloseInfoFrame() => CloseInfoFrame(null, null); - - private bool CloseInfoFrame(GUIButton button, object userData) - { - infoBox = null; - infoBoxClosedCallback?.Invoke(); - return true; - } - - /// - // Creates and displays a tutorial info box - /// - private GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) - { - if (hasButton) - { - height += 60; - } - - width = (int)(width * GUI.Scale); - height = (int)(height * GUI.Scale); - - LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); - height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; - - if (title.Length > 0) - { - height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); - } - - var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); - - var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); - infoBlock.Flash(GUIStyle.Green); - - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) - { - Stretch = true, - AbsoluteSpacing = 5 - }; - - if (title.Length > 0) - { - var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), - title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); - titleBlock.RectTransform.IsFixedSize = true; - } - - text = RichString.Rich(text); - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - - textBlock.RectTransform.IsFixedSize = true; - infoBoxClosedCallback = onInfoBoxClosed; - - if (hasButton) - { - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.1f - }; - buttonContainer.RectTransform.IsFixedSize = true; - - if (onVideoButtonClicked != null) - { - buttonContainer.Stretch = true; - var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("Video"), style: "GUIButtonLarge") - { - OnClicked = (GUIButton button, object obj) => - { - onVideoButtonClicked(); - return true; - } - }; - } - else - { - buttonContainer.Stretch = false; - buttonContainer.ChildAnchor = Anchor.Center; - } - - var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), - TextManager.Get("OK"), style: "GUIButtonLarge") - { - OnClicked = CloseInfoFrame - }; - } - - infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); - - SoundPlayer.PlayUISound(GUISoundType.UIMessage); - - return background; - } - - #endregion - - #region Video - - private void LoadVideo(Segment segment) - { - videoPlayer ??= new VideoPlayer(); - if (segment.AutoPlayVideo == AutoPlayVideo.Yes) - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), - contentId: segment.Id, - startPlayback: true, - objective: segment.ObjectiveText, - onStop: StopCurrentContentSegment); - } - else - { - videoPlayer.LoadContent( - contentPath: segment.VideoContent.ContentPath, - videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), - textSettings: null, - contentId: segment.Id, - startPlayback: true, - objective: string.Empty); - } - } - - private void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); - - #endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index d155a1c23..3a8b4a7f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public Tutorial Tutorial; + public override bool Paused => Tutorial.Paused; + public TutorialMode(GameModePreset preset) : base(preset) { } public override void Start() @@ -19,12 +21,6 @@ namespace Barotrauma } } - public override void AddToGUIUpdateList() - { - base.AddToGUIUpdateList(); - Tutorial.AddToGUIUpdateList(); - } - public override void Update(float deltaTime) { base.Update(deltaTime); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index dd566ee34..90226c5e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma @@ -128,8 +129,9 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } GameMode?.AddToGUIUpdateList(); tabMenu?.AddToGUIUpdateList(); + ObjectiveManager.AddToGUIUpdateList(); - if ((!(GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && + if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) && !CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { if (topLeftButtonGroup == null) @@ -223,6 +225,7 @@ namespace Barotrauma } HintManager.Update(); + ObjectiveManager.VideoPlayer.Update(); } public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 0dfc29ed7..fcb5438dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -1,6 +1,7 @@ using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; +using Barotrauma.Tutorials; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -209,7 +210,7 @@ namespace Barotrauma { if (item.CurrentHull == null) { continue; } if (item.GetComponent() == null) { continue; } - if (!item.HasTag("ballast")) { continue; } + if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } BallastHulls.Add(item.CurrentHull); } } @@ -383,6 +384,34 @@ namespace Barotrauma IgnoreReminder("tabmenu"); } + public static void OnObtainedItem(Character character, Item item) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || item == null) { return; } + + if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; } + foreach (Identifier tag in item.GetTags()) + { + if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; } + } + + if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) || + (item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null)) + { + if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; } + } + } + + public static void OnStartDeconstructing(Character character, Deconstructor deconstructor) + { + if (!CanDisplayHints()) { return; } + if (character != Character.Controlled || deconstructor == null) { return; } + if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent() is not null)) + { + DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier()); + } + } + public static void OnStoleItem(Character character, Item item) { if (!CanDisplayHints()) { return; } @@ -507,7 +536,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints - if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } + if (component is not Repairable || actionType != ActionType.OnFailure) { return; } DisplayHint("onrepairfailed".ToIdentifier()); } @@ -563,7 +592,7 @@ namespace Barotrauma foreach (var me in gap.linkedTo) { if (me == Character.Controlled.CurrentHull) { continue; } - if (!(me is Hull adjacentHull)) { continue; } + if (me is not Hull adjacentHull) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } @@ -720,6 +749,7 @@ namespace Barotrauma if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; } + if (ObjectiveManager.AnyObjectives) { return false; } if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index c06efe8d5..232e84838 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum RequestResult { @@ -19,63 +19,11 @@ namespace Barotrauma Timeout } - public readonly struct RequestAction - { - public readonly Action Callback; - public readonly DateTimeOffset Timeout; - - public RequestAction(Action callback, DateTimeOffset timeout) - { - Callback = callback; - Timeout = timeout; - } - } - - public readonly struct AfflictionRequest - { - public readonly RequestResult Result; - public readonly ImmutableArray Afflictions; - - public AfflictionRequest(RequestResult result, ImmutableArray afflictions) - { - Result = result; - Afflictions = afflictions; - } - } - - public readonly struct PendingRequest - { - public readonly RequestResult Result; - public readonly ImmutableArray CrewMembers; - - public PendingRequest(RequestResult result, ImmutableArray crewMembers) - { - Result = result; - CrewMembers = crewMembers; - } - } - - public readonly struct CallbackOnlyRequest - { - public readonly RequestResult Result; - - public CallbackOnlyRequest(RequestResult result) - { - Result = result; - } - } - - public readonly struct HealRequest - { - public readonly RequestResult Result; - public readonly HealRequestResult HealResult; - - public HealRequest(RequestResult result, HealRequestResult healResult) - { - Result = result; - HealResult = healResult; - } - } + public readonly record struct RequestAction(Action Callback, DateTimeOffset Timeout); + public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray Afflictions); + public readonly record struct PendingRequest(RequestResult Result, NetCollection CrewMembers); + public readonly record struct CallbackOnlyRequest(RequestResult Result); + public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult); private readonly List> afflictionRequests = new List>(); private readonly List> pendingHealRequests = new List>(); @@ -96,7 +44,7 @@ namespace Barotrauma } #endif - if (!(info is { Character: { CharacterHealth: { } health } })) + if (info is not { Character.CharacterHealth: { } health }) { onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); return; @@ -123,14 +71,14 @@ namespace Barotrauma public void Update(float deltaTime) { DateTimeOffset now = DateTimeOffset.Now; - UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); - UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); - UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); + UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection.Empty)); }); + UpdateQueue(healAllRequests, now, onTimeout: static callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); - void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } + static void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } } public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) @@ -148,9 +96,9 @@ namespace Barotrauma private static bool TryDequeue(List> requestQueue, out Action result) { RequestAction? first = requestQueue.FirstOrNull(); - if (!(first is { } action)) + if (first is not { } action) { - result = _ => { }; + result = static _ => { }; return false; } @@ -191,11 +139,25 @@ namespace Barotrauma private static int GetPing() { - if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } + if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; } return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); } + public void TreatAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + AddEverythingToPending(); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); + } + public void HealAllButtonAction(Action onReceived) { if (GameMain.IsSingleplayer) @@ -296,8 +258,11 @@ namespace Barotrauma private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) { - NetCrewMember crewMember = INetSerializableStruct.Read(inc); - InsertPendingCrewMember(crewMember); + var crewMembers = INetSerializableStruct.Read>(inc); + foreach (var crewMember in crewMembers) + { + InsertPendingCrewMember(crewMember); + } if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) { callback(new CallbackOnlyRequest(RequestResult.Success)); @@ -318,11 +283,7 @@ namespace Barotrauma private static void SendAfflictionRequest(CharacterInfo info) { - INetSerializableStruct crewMember = new NetCrewMember - { - CharacterInfo = info, - Afflictions = Array.Empty() - }; + INetSerializableStruct crewMember = new NetCrewMember(info); ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); } @@ -337,17 +298,17 @@ namespace Barotrauma NetCrewMember crewMember = INetSerializableStruct.Read(inc); if (TryDequeue(afflictionRequests, out var callback)) { - RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success; + RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); } } private void PendingRequestReceived(IReadMessage inc) { - NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); + var pendingCrew = INetSerializableStruct.Read>(inc); if (TryDequeue(pendingHealRequests, out var callback)) { - callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); + callback(new PendingRequest(RequestResult.Success, pendingCrew)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs new file mode 100644 index 000000000..7e70ed7f5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ObjectiveManager.cs @@ -0,0 +1,599 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma; + +static class ObjectiveManager +{ + public class Segment + { + public readonly record struct Text( + Identifier Tag, + int Width = DefaultWidth, + int Height = DefaultHeight, + Anchor Anchor = Anchor.Center); + + public readonly record struct Video( + string FullPath, + Identifier TextTag, + int Width = DefaultWidth, + int Height = DefaultHeight) + { + public string FileName => Path.GetFileName(FullPath.CleanUpPath()); + public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath()); + } + + private const int DefaultWidth = 450; + private const int DefaultHeight = 80; + + public GUIImage ObjectiveStateIndicator; + public GUIButton ObjectiveButton; + public GUITextBlock LinkedTextBlock; + public LocalizedString ObjectiveText; + + public readonly Identifier Id; + public readonly Text TextContent; + public readonly Video VideoContent; + public readonly AutoPlayVideo AutoPlayVideo; + + public Action OnClickObjective; + + public bool IsCompleted { get; set; } + + public bool CanBeCompleted { get; set; } + + public Identifier ParentId { get; set; } + + public TutorialSegmentType SegmentType { get; private set; } + + public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent); + } + + public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + return new Segment(id, objectiveTextTag, onClickObjective); + } + + public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag) + { + return new Segment(id, objectiveTextTag); + } + + private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + AutoPlayVideo = autoPlayVideo; + TextContent = textContent; + VideoContent = videoContent; + SegmentType = TutorialSegmentType.InfoBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + OnClickObjective = onClickObjective; + SegmentType = TutorialSegmentType.MessageBox; + } + + private Segment(Identifier id, Identifier objectiveTextTag) + { + Id = id; + ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + SegmentType = TutorialSegmentType.Objective; + } + + public void ConnectMessageBox(Segment messageBoxSegment) + { + SegmentType = TutorialSegmentType.MessageBox; + OnClickObjective = messageBoxSegment.OnClickObjective; + } + } + + private readonly record struct ScreenSettings( + Point ScreenResolution = default, + float UiScale = default, + WindowMode WindowMode = default) + { + public bool HaveChanged() => + GameMain.GraphicsWidth != ScreenResolution.X || + GameMain.GraphicsHeight != ScreenResolution.Y || + GUI.Scale != UiScale || + GameSettings.CurrentConfig.Graphics.DisplayMode != WindowMode; + }; + + private const float ObjectiveComponentAnimationTime = 1.5f; + + public static bool ContentRunning { get; private set; } + + public static VideoPlayer VideoPlayer { get; } = new VideoPlayer(); + + private static Segment ActiveContentSegment { get; set; } + + private readonly static List activeObjectives = new List(); + private static GUIComponent infoBox; + private static Action infoBoxClosedCallback; + private static ScreenSettings screenSettings; + private static GUILayoutGroup objectiveGroup; + private static LocalizedString objectiveTextTranslated; + + public static void AddToGUIUpdateList() + { + if (screenSettings.HaveChanged()) + { + CreateObjectiveFrame(); + } + if (activeObjectives.Count > 0 && GameMain.GameSession?.Campaign is not { ShowCampaignUI: true }) + { + objectiveGroup?.AddToGUIUpdateList(order: -1); + } + infoBox?.AddToGUIUpdateList(order: 100); + VideoPlayer.AddToGUIUpdateList(order: 100); + } + + public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false) + { + if (segment.SegmentType != TutorialSegmentType.InfoBox) + { + activeObjectives.Add(segment); + AddToObjectiveList(segment, connectObjective); + return; + } + + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + + var title = TextManager.Get(segment.Id); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag); + tutorialText = TextManager.ParseInputTypes(tutorialText); + + switch (segment.AutoPlayVideo) + { + case AutoPlayVideo.Yes: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: LoadActiveContentVideo); + break; + case AutoPlayVideo.No: + infoBox = CreateInfoFrame( + title, + tutorialText, + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: StopCurrentContentSegment, + onVideoButtonClicked: LoadActiveContentVideo); + break; + } + } + + public static void CompleteTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted) + { + return; + } + if (!MarkSegmentCompleted(segment)) + { + return; + } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed"); + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed"); + campaign?.CampaignMetadata?.SetValue(segmentId, true); + } + } + + public static bool MarkSegmentCompleted(Segment segment, bool flash = true) + { + segment.IsCompleted = true; + if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) + { + if (segment.ObjectiveStateIndicator.Style == style) + { + return false; + } + segment.ObjectiveStateIndicator.ApplyStyle(style); + } + if (flash) + { + segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true); + } + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + return true; + } + + public static void RemoveTutorialSegment(Identifier segmentId) + { + if (GetActiveObjective(segmentId) is not Segment segment) + { + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode) + { + DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!"); + } + return; + } + segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false); + segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false); + var parent = segment.LinkedTextBlock.Parent; + parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () => + { + activeObjectives.Remove(segment); + objectiveGroup?.Recalculate(); + }); + parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime); + segment.ObjectiveButton.OnClicked = null; + segment.ObjectiveButton.CanBeFocused = false; + } + + public static void CloseActiveContentGUI() + { + if (VideoPlayer.IsPlaying) + { + VideoPlayer.Stop(); + } + else if (infoBox != null) + { + CloseInfoFrame(); + } + } + + public static void ClearContent() + { + ContentRunning = false; + infoBox = null; + } + + public static void ResetUI() + { + ContentRunning = false; + infoBox = null; + VideoPlayer.Remove(); + } + + #region Objectives + private static Segment GetActiveObjective(Identifier id) => activeObjectives.FirstOrDefault(s => s.Id == id); + + public static void ResetObjectives() + { + activeObjectives.Clear(); + ActiveContentSegment = null; + CreateObjectiveFrame(); + } + + /// + /// Create the objective list that holds the objectives (called on start and on resolution change) + /// + private static void CreateObjectiveFrame() + { + var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null) + { + CanBeFocused = false + }; + objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform)) + { + AbsoluteSpacing = (int)GUIStyle.Font.LineHeight + }; + for (int i = 0; i < activeObjectives.Count; i++) + { + AddToObjectiveList(activeObjectives[i]); + } + screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode); + } + + /// + /// Stops content running and adds the active segment to the objective list + /// + private static void StopCurrentContentSegment() + { + if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty()) + { + activeObjectives.Add(ActiveContentSegment); + AddToObjectiveList(ActiveContentSegment); + } + ContentRunning = false; + ActiveContentSegment = null; + } + + /// + /// Adds the segment to the objective list + /// + private static void AddToObjectiveList(Segment segment, bool connectExisting = false) + { + if (connectExisting) + { + if (activeObjectives.Find(o => o.Id == segment.Id) is { } existingSegment) + { + existingSegment.ConnectMessageBox(segment); + SetButtonBehavior(existingSegment); + } + return; + } + + var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) + { + MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) + }; + Segment parentSegment = activeObjectives.FirstOrDefault(s => s.Id == segment.ParentId); + if (parentSegment is not null) + { + // Add this child as the last child in case there are other existing children already + int totalChildren = activeObjectives.Count(s => s.ParentId == segment.ParentId); + int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren; + if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex) + { + frameRt.RepositionChildInHierarchy(childIndex); + activeObjectives.Remove(segment); + activeObjectives.Insert(childIndex, segment); + } + } + frameRt.AbsoluteOffset = GetObjectiveHiddenPosition(); + + var frame = new GUIFrame(frameRt, style: null) + { + CanBeFocused = true + }; + + objectiveGroup.Recalculate(); + + int textWidth = parentSegment is null ? frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing + : frameRt.Rect.Width - 2 * objectiveGroup.AbsoluteSpacing; + segment.LinkedTextBlock = new GUITextBlock( + new RectTransform(new Point(textWidth, 0), frame.RectTransform, anchor: Anchor.TopRight), + TextManager.ParseInputTypes(segment.ObjectiveText), + wrap: true); + + var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height); + segment.LinkedTextBlock.RectTransform.NonScaledSize = size; + segment.LinkedTextBlock.RectTransform.MinSize = size; + segment.LinkedTextBlock.RectTransform.MaxSize = size; + segment.LinkedTextBlock.RectTransform.IsFixedSize = true; + frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false); + frame.RectTransform.IsFixedSize = true; + + var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true); + if (parentSegment is not null) + { + indicatorRt.AbsoluteOffset = new Point(objectiveGroup.AbsoluteSpacing, 0); + } + segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete"); + + SetTransparent(segment.LinkedTextBlock); + + objectiveTextTranslated ??= TextManager.Get("Tutorial.Objective"); + segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null) + { + ToolTip = objectiveTextTranslated + }; + SetButtonBehavior(segment); + SetTransparent(segment.ObjectiveButton); + + frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate()); + + // Check if the objective has already been completed in the campaign + if (!segment.IsCompleted && GameMain.GameSession?.Campaign?.CampaignMetadata is CampaignMetadata data && data.GetBoolean(segment.Id)) + { + MarkSegmentCompleted(segment, flash: false); + } + + static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent; + + void SetButtonBehavior(Segment segment) + { + segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective; + segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) => + { + if (segment.SegmentType == TutorialSegmentType.InfoBox) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + ReplaySegmentVideo(segment); + } + else + { + ShowSegmentText(segment); + } + } + else if (segment.SegmentType == TutorialSegmentType.MessageBox) + { + segment.OnClickObjective?.Invoke(); + } + return true; + }; + } + } + + private static void ReplaySegmentVideo(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + LoadVideo(segment); + } + + private static void ShowSegmentText(Segment segment) + { + if (ContentRunning) { return; } + Inventory.DraggingItems.Clear(); + ContentRunning = true; + ActiveContentSegment = segment; + infoBox = CreateInfoFrame( + TextManager.Get(segment.Id), + TextManager.Get(segment.TextContent.Tag), + segment.TextContent.Width, + segment.TextContent.Height, + segment.TextContent.Anchor, + hasButton: true, + onInfoBoxClosed: () => ContentRunning = false, + onVideoButtonClicked: () => LoadVideo(segment)); + } + + private static Point GetObjectiveHiddenPosition(RectTransform rt = null) + { + return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0); + } + + public static Segment GetObjective(Identifier identifier) + { + return activeObjectives.FirstOrDefault(o => o.Id == identifier); + } + + public static bool AllActiveObjectivesCompleted() + { + return activeObjectives.None() || activeObjectives.All(o => !o.CanBeCompleted || o.IsCompleted); + } + + public static bool AnyObjectives => activeObjectives.Any(); + + #endregion + + #region InfoFrame + + private static void CloseInfoFrame() => CloseInfoFrame(null, null); + + private static bool CloseInfoFrame(GUIButton button, object userData) + { + infoBox = null; + infoBoxClosedCallback?.Invoke(); + return true; + } + + /// + // Creates and displays a tutorial info box + /// + private static GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null) + { + if (hasButton) + { + height += 60; + } + + width = (int)(width * GUI.Scale); + height = (int)(height * GUI.Scale); + + LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); + height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; + + if (title.Length > 0) + { + height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); + } + + var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); + infoBlock.Flash(GUIStyle.Green); + + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) + { + Stretch = true, + AbsoluteSpacing = 5 + }; + + if (title.Length > 0) + { + var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), + title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + titleBlock.RectTransform.IsFixedSize = true; + } + + text = RichString.Rich(text); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); + + textBlock.RectTransform.IsFixedSize = true; + infoBoxClosedCallback = onInfoBoxClosed; + + if (hasButton) + { + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.1f + }; + buttonContainer.RectTransform.IsFixedSize = true; + + if (onVideoButtonClicked != null) + { + buttonContainer.Stretch = true; + var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("Video"), style: "GUIButtonLarge") + { + OnClicked = (GUIButton button, object obj) => + { + onVideoButtonClicked(); + return true; + } + }; + } + else + { + buttonContainer.Stretch = false; + buttonContainer.ChildAnchor = Anchor.Center; + } + + var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("OK"), style: "GUIButtonLarge") + { + OnClicked = CloseInfoFrame + }; + } + + infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y)); + + SoundPlayer.PlayUISound(GUISoundType.UIMessage); + + return background; + } + + #endregion + + #region Video + + private static void LoadVideo(Segment segment) + { + if (segment.AutoPlayVideo == AutoPlayVideo.Yes) + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width), + contentId: segment.Id, + startPlayback: true, + objective: segment.ObjectiveText, + onStop: StopCurrentContentSegment); + } + else + { + VideoPlayer.LoadContent( + contentPath: segment.VideoContent.ContentPath, + videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName), + textSettings: null, + contentId: segment.Id, + startPlayback: true, + objective: string.Empty); + } + } + + private static void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment); + + #endregion +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index a4ec49919..361685dde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -21,7 +21,6 @@ namespace Barotrauma private readonly GameMode gameMode; - private readonly float initialLocationReputation; private readonly Dictionary initialFactionReputations = new Dictionary(); public GUILayoutGroup ButtonArea { get; private set; } @@ -36,7 +35,6 @@ 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) @@ -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)) { @@ -401,7 +400,7 @@ namespace Barotrauma }; reputationList.ContentBackground.Color = Color.Transparent; - if (startLocation.Type.HasOutpost && startLocation.Reputation != null) + /*if (startLocation.Type.HasOutpost && startLocation.Reputation != null) { var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); var locationFrame = CreateReputationElement( @@ -411,7 +410,7 @@ namespace Barotrauma 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)) { @@ -462,27 +461,25 @@ namespace Barotrauma if (!connection.Locked || (!connection.Locations[0].Discovered && !connection.Locations[1].Discovered)) { 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 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.Faction.IsEmpty) { if (location == null || gateLocation != location) { continue; } } else { - if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } + if (faction == null || faction.Prefab.Identifier != unlockEvent.Faction) { continue; } } if (unlockEvent != null) { Reputation unlockReputation = gateLocation.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; } float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); @@ -506,7 +503,7 @@ namespace Barotrauma private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { - string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; + string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name; string textTag; if (gameOver) @@ -543,6 +540,11 @@ namespace Barotrauma } } + if (startLocation?.Biome != null && startLocation.Biome.IsEndBiome) + { + locationName ??= startLocation.Name; + } + if (textTag == null) { return ""; } if (locationName == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 1645e1be1..714a96576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -774,7 +774,6 @@ namespace Barotrauma } else { - bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedItem?.GetComponent(); if (selectedContainer != null && @@ -802,8 +801,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - /*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/ - ((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + (i.OwnInventory.CanBePut(item) || ((i.OwnInventory.Capacity == 1 || i.OwnInventory.Container.HasSubContainers) && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } @@ -977,7 +975,7 @@ namespace Barotrauma heldItem.OwnInventory.GetItemAt(0)?.Prefab == item.Prefab && heldItem.OwnInventory.GetItemsAt(0).Count() > 1; if (heldItem.OwnInventory.TryPutItem(item, Character.Controlled) || - (heldItem.OwnInventory.Capacity == 1 && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) + ((heldItem.OwnInventory.Capacity == 1 || heldItem.OwnInventory.Container.HasSubContainers) && heldItem.OwnInventory.TryPutItem(item, 0, allowSwapping: !disallowSwapping, allowCombine: false, user: Character.Controlled))) { success = true; for (int j = 0; j < capacity; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index 1684642d0..f649e423c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -25,14 +25,31 @@ namespace Barotrauma.Items.Components foreach (Node node in nodes) { GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero); + + if (node.ParentIndex > -1) + { + Vector2 diff = nodes[node.ParentIndex].WorldPosition - node.WorldPosition; + float dist = diff.Length(); + Vector2 normalizedDiff = diff / dist; + for (float x = 0.0f; x < dist; x += 50.0f) + { + var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", node.WorldPosition + normalizedDiff * x, Vector2.Zero); + if (spark != null) + { + spark.Size *= 0.3f; + } + } + + } } } public void DrawElectricity(SpriteBatch spriteBatch) { + if (timer <= 0.0f) { return; } for (int i = 0; i < nodes.Count; i++) { - if (nodes[i].Length <= 1.0f) continue; + if (nodes[i].Length <= 1.0f) { continue; } var node = nodes[i]; electricitySprite.Draw(spriteBatch, (i + frameOffset) % electricitySprite.FrameCount, @@ -46,10 +63,16 @@ namespace Barotrauma.Items.Components if (GameMain.DebugDraw) { - for (int i = 0; i < nodes.Count; i++) + for (int i = 1; i < nodes.Count; i++) { - if (nodes[i].Length <= 1.0f) continue; - GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 5, Color.LightCyan, isFilled: true); + GUI.DrawLine(spriteBatch, + new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), + new Vector2(nodes[nodes[i].ParentIndex].WorldPosition.X, -nodes[nodes[i].ParentIndex].WorldPosition.Y), + Color.LightCyan, + width: 3); + + if (nodes[i].Length <= 1.0f) { continue; } + GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 10, Color.LightCyan, isFilled: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 8a605499d..fd8f360eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -114,8 +114,8 @@ namespace Barotrauma.Items.Components { if (chargeSound != null) { - chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); - if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); + if (chargeSoundChannel != null) { chargeSoundChannel.Looping = true; } } } else if (chargeSoundChannel != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index f97cdc3be..539273366 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -224,68 +224,60 @@ namespace Barotrauma.Items.Components if (character == null) { return false; } if (character == Character.Controlled) { - if (targetSections.Count == 0) { return false; } - Spray(deltaTime); + if (targetSections.Count == 0) { return false; } + Spray(character, deltaTime, applyColors: true); return true; } else { //allow remote players to use the sprayer, but don't actually color the walls (we'll receive the data from the server) - return character.IsRemotePlayer; + Spray(character, deltaTime, applyColors: false); + return true; } } - public void Spray(float deltaTime) + public void Spray(Character user, float deltaTime, bool applyColors) { - if (targetSections.Count == 0) { return; } - Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); if (liquidItem == null) { return; } bool isCleaning = false; liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); - // Ethanol or other cleaning solvent - if (color.A == 0) { isCleaning = true; } - - float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; - - if (!isCleaning) + if (applyColors && targetSections.Any()) { - for (int i = 0; i < targetSections.Count; i++) + // Ethanol or other cleaning solvent + if (color.A == 0) { isCleaning = true; } + float sizeAdjustedSprayStrength = SprayStrength / targetSections.Count; + if (!isCleaning) { - targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.IncreaseSectionColorOrStrength(targetSections[i], color, sizeAdjustedSprayStrength * deltaTime, true, false); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentCleaning += deltaTime; + } } - if (GameMain.GameSession != null) + else { - GameMain.GameSession.TimeSpentCleaning += deltaTime; - } - } - else - { - for (int i = 0; i < targetSections.Count; i++) - { - targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); - } - if (GameMain.GameSession != null) - { - GameMain.GameSession.TimeSpentPainting += deltaTime; + for (int i = 0; i < targetSections.Count; i++) + { + targetHull.CleanSection(targetSections[i], -sizeAdjustedSprayStrength * deltaTime, true); + } + if (GameMain.GameSession != null) + { + GameMain.GameSession.TimeSpentPainting += deltaTime; + } } } Vector2 particleStartPos = item.WorldPosition + ConvertUnits.ToDisplayUnits(TransformedBarrelPos); - Vector2 particleEndPos = Vector2.Zero; - for (int i = 0; i < targetSections.Count; i++) - { - particleEndPos += new Vector2(targetSections[i].Rect.Center.X, targetSections[i].Rect.Y - targetSections[i].Rect.Height / 2) + targetHull.Rect.Location.ToVector2(); - } - particleEndPos /= targetSections.Count; - if (targetHull?.Submarine != null) - { - particleEndPos += targetHull.Submarine.Position; - } - float dist = Vector2.Distance(particleStartPos, particleEndPos); - + Vector2 particleEndPos = user.CursorWorldPosition; + //the cursor position is not exact for remote players, we only know the direction they're aiming at but not the distance + // -> use 50% range, looks good enough + float dist = Math.Min(Vector2.Distance(particleStartPos, particleEndPos), Range * 0.5f); foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 345411caa..5f1a13b2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -524,11 +524,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 +586,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 +627,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 +652,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 d43d947fd..d8c379c03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -309,21 +309,53 @@ namespace Barotrauma.Items.Components Vector2 currentItemPos = transformedItemPos; - SpriteEffects spriteEffects = SpriteEffects.None; - if ((item.body != null && item.body.Dir == -1) || item.FlippedX) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; - } - if (item.FlippedY) - { - spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; - } - bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; foreach (Item containedItem in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(containedItem); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + 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) + { + 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; + } + } + } + } + if (containedItem?.Sprite == null) { continue; } if (AutoInteractWithContained) @@ -343,19 +375,34 @@ namespace Barotrauma.Items.Components } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; + SpriteEffects spriteEffects = SpriteEffects.None; + float spriteRotation = ItemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + spriteRotation = relatedItem.Rotation; + } + if ((item.body != null && item.body.Dir == -1) || item.FlippedX) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipVertically : SpriteEffects.FlipHorizontally; + } + if (item.FlippedY) + { + spriteEffects |= MathUtils.NearlyEqual(spriteRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; + } + containedItem.Sprite.Draw( spriteBatch, - new Vector2(currentItemPos.X, -currentItemPos.Y), + new Vector2(itemPos.X, -itemPos.Y), isWiringMode ? containedItem.GetSpriteColor(withHighlight: true) * 0.15f : containedItem.GetSpriteColor(withHighlight: true), origin, - -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation ), + -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation), containedItem.Scale, spriteEffects, depth: containedSpriteDepth); foreach (ItemContainer ic in containedItem.GetComponents()) { - if (ic.hideItems) continue; + 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 332f924cd..1d4db25d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { - if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX && item.body == null) + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX) { Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3b25c87b0..c0b717496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1119,7 +1119,7 @@ namespace Barotrauma.Items.Components if (it.GetComponent() is { } battery) { - int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f); + int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f); line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString()); } else if (it.GetComponent() is { } powerTransfer) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index b6ff975de..fcb4e97c7 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 } @@ -811,7 +816,7 @@ namespace Barotrauma.Items.Components if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); - if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) + if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter)) { Ping(t.WorldPosition, transducerCenter, Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), @@ -971,7 +976,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, @@ -981,7 +986,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, @@ -1006,19 +1011,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++; } @@ -1276,7 +1281,7 @@ namespace Barotrauma.Items.Components float indicatorSector = sector * 0.75f; float indicatorSectorLength = (float)(midLength / Math.Cos(indicatorSector)); - bool withinSector = + bool withinSector = (Math.Abs(diff.X) < steering.ActiveDockingSource.DistanceTolerance.X && Math.Abs(diff.Y) < steering.ActiveDockingSource.DistanceTolerance.Y) || Vector2.Dot(normalizedDockingDir, MathUtils.RotatePoint(normalizedDockingDir, indicatorSector)) < Vector2.Dot(normalizedDockingDir, Vector2.Normalize(dockingDir)); @@ -1344,6 +1349,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) { @@ -1388,6 +1425,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]); @@ -1586,6 +1631,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 7f06e5dbf..c5e8531f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components private bool? swapDestinationOrder; - private GUIMessageBox enterOutpostPrompt; + private GUIMessageBox enterOutpostPrompt, exitOutpostPrompt; private bool levelStartSelected; public bool LevelStartSelected @@ -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(); }; @@ -382,15 +386,26 @@ namespace Barotrauma.Items.Components DockingSources.Any(d => d.Docked && (d.DockingTarget?.Item.Submarine?.Info?.IsOutpost ?? false))) { // Undocking from an outpost - campaign.ShowCampaignUI = true; - campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); - return false; + if (!ObjectiveManager.AllActiveObjectivesCompleted()) + { + exitOutpostPrompt = new GUIMessageBox("", + TextManager.GetWithVariable("CampaignExitTutorialOutpostPrompt", "[locationname]", campaign.Map.CurrentLocation.Name), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); + exitOutpostPrompt.Buttons[0].OnClicked += (_, _) => + { + exitOutpostPrompt.Close(); + return OpenMap(campaign); + }; + exitOutpostPrompt.Buttons[1].OnClicked += exitOutpostPrompt.Close; + return false; + } + return OpenMap(campaign); } else if (!Level.IsLoadedOutpost && DockingModeEnabled && ActiveDockingSource != null && !ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false)) { // Docking to an outpost - var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine); + var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine); if (subsToLeaveBehind.Any()) { enterOutpostPrompt = new GUIMessageBox( @@ -419,6 +434,14 @@ namespace Barotrauma.Items.Components return true; } }; + + bool OpenMap(CampaignMode campaign) + { + campaign.ShowCampaignUI = true; + campaign.CampaignUI.SelectTab(CampaignMode.InteractionType.Map); + return false; + } + void SendDockingSignal() { if (GameMain.Client == null) @@ -431,6 +454,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } } + dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; @@ -913,6 +937,7 @@ namespace Barotrauma.Items.Components maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); enterOutpostPrompt?.Close(); + exitOutpostPrompt?.Close(); pathFinder = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 72070c051..c3f44c01d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -133,35 +133,43 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; } + Vector2 scaledIndicatorSize = indicatorSize * item.Scale; + if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; } + const float outlineThickness = 1.0f; Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale; - Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale; - if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; } - if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; } + Vector2 indicatorPos = -itemSize / 2.0f + indicatorPosition * item.Scale; + Vector2 itemPosition = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y); + Vector2 flip = new Vector2(item.FlippedX && item.Prefab.CanSpriteFlipX ? -1.0f : 1.0f, item.FlippedY && item.Prefab.CanSpriteFlipY ? -1.0f : 1.0f); + Matrix rotate = Matrix.CreateRotationZ(item.RotationRad); + Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition; if (charge > 0 && capacity > 0) { float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f); Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green); - if (!isHorizontal) + Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip; + Vector2 indicatorSize; + + if (isHorizontal) { - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos, - new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + float indicatorLength = (scaledIndicatorSize.X - outlineThickness * 2.0f) * chargeRatio; + indicatorCenter.X += -scaledIndicatorSize.X * 0.5f + (flipIndicator ? scaledIndicatorSize.X - outlineThickness - indicatorLength * 0.5f : outlineThickness + indicatorLength * 0.5f); + indicatorSize = new Vector2(indicatorLength, scaledIndicatorSize.Y); } else { - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true, - depth: item.SpriteDepth - 0.00001f); + float indicatorLength = (scaledIndicatorSize.Y - outlineThickness * 2.0f) * chargeRatio; + indicatorCenter.Y += -scaledIndicatorSize.Y * 0.5f + (flipIndicator ? outlineThickness + indicatorLength * 0.5f : scaledIndicatorSize.Y - outlineThickness - indicatorLength * 0.5f); + indicatorSize = new Vector2(scaledIndicatorSize.X, indicatorLength); } + + indicatorCenter = Vector2.Transform(indicatorCenter, rotate) + itemPosition; + + GUI.DrawFilledRectangle(spriteBatch, indicatorCenter, indicatorSize, indicatorSize * 0.5f, item.RotationRad, indicatorColor, item.SpriteDepth - 0.00001f); } - GUI.DrawRectangle(spriteBatch, - new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos, - indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f); + + GUI.DrawRectangle(spriteBatch, center, scaledIndicatorSize, scaledIndicatorSize * 0.5f, item.RotationRad, Color.Black, item.SpriteDepth - 0.000015f, outlineThickness, GUI.OutlinePosition.Inside); } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData) 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 d6e5500c3..afe938020 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -293,7 +293,7 @@ namespace Barotrauma.Items.Components if (target.Bleeding > 0.0f) { - int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f) * BleedingTexts.Length, 0, BleedingTexts.Length - 1); + int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f * BleedingTexts.Length), 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 03e4e2eff..701a5474e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -212,14 +212,14 @@ namespace Barotrauma.Items.Components { if (moveSoundChannel == null && startMoveSound != null) { - moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling, freqMult: startMoveSound.GetRandomFrequencyMultiplier()); } else if (moveSoundChannel == null || !moveSoundChannel.IsPlaying) { if (moveSound != null) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling, freqMult: moveSound.GetRandomFrequencyMultiplier()); if (moveSoundChannel != null) moveSoundChannel.Looping = true; } } @@ -231,7 +231,7 @@ namespace Barotrauma.Items.Components if (endMoveSound != null && moveSoundChannel.Sound != endMoveSound.Sound) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling); + moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling, freqMult: endMoveSound.GetRandomFrequencyMultiplier()); if (moveSoundChannel != null) moveSoundChannel.Looping = false; } else if (!moveSoundChannel.IsPlaying) @@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components { if (chargeSound != null) { - chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling, freqMult: chargeSound.GetRandomFrequencyMultiplier()); if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; } } @@ -581,7 +581,7 @@ namespace Barotrauma.Items.Components var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } availableCharge += battery.Charge; - availableCapacity += battery.Capacity; + availableCapacity += battery.GetCapacity(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 4d4893e4e..58093440a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Barotrauma.Networking; @@ -6,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { - private void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) + private static void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } @@ -19,8 +20,13 @@ namespace Barotrauma.Items.Components if (!description.IsNullOrWhiteSpace()) { description += '\n'; } description += $" ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } - + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) + { + AddTooltipInfo(damageModifiers, SkillModifiers, ref description); + } + + public static void AddTooltipInfo(IReadOnlyList damageModifiers, IReadOnlyDictionary skillModifiers, ref LocalizedString description) { if (damageModifiers.Any()) { @@ -41,9 +47,9 @@ namespace Barotrauma.Items.Components } } } - if (SkillModifiers.Any()) + if (skillModifiers.Any()) { - foreach (var skillModifier in SkillModifiers) + foreach (var skillModifier in skillModifiers) { string colorStr = XMLExtensions.ToStringHex(GUIStyle.Green); int roundedValue = (int)Math.Round(skillModifier.Value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 7858e3154..0694c838a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1610,20 +1610,26 @@ namespace Barotrauma } else if (itemContainer.ShowTotalStackCapacityInContainedStateIndicator) { - containedState = itemContainer.Inventory.AllItems.Count() / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.Capacity); + int ignoredItems = itemContainer.AllSubContainableItems == null ? 0 : itemContainer.AllSubContainableItems.Count; + int itemCount = itemContainer.Inventory.AllItems.Count() - ignoredItems; + containedState = itemCount / (float)(itemContainer.GetMaxStackSize(0) * itemContainer.MainContainerCapacity); } else { - var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); + + int targetSlot = Math.Max(itemContainer.ContainedStateIndicatorSlot, 0); + var containedItem = itemContainer.Inventory.slots[targetSlot].FirstOrDefault(); + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; - if (containedItem != null && itemContainer.Inventory.Capacity == 1) + + if (containedItem != null && (itemContainer.Inventory.Capacity == 1 || itemContainer.HasSubContainers)) { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(targetSlot)); if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) { - containedState = itemContainer.Inventory.slots[0].Items.Count / (float)maxStackSize; + containedState = itemContainer.Inventory.slots[targetSlot].Items.Count / (float)maxStackSize; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0c35c29eb..682fed935 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -301,7 +301,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++) @@ -417,6 +419,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) @@ -1415,6 +1419,15 @@ namespace Barotrauma case EventType.ChangeProperty: ReadPropertyChange(msg, false); break; + case EventType.ItemStat: + byte length = msg.ReadByte(); + for (int i = 0; i < length; i++) + { + var statIdentifier = INetSerializableStruct.Read(msg); + var statValue = msg.ReadSingle(); + StatManager.ApplyStat(statIdentifier, statValue); + } + break; case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); @@ -1654,25 +1667,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(); @@ -1776,6 +1788,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 8c6aed81b..a39cd80e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -1,5 +1,5 @@ -using Barotrauma.IO; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -73,6 +72,9 @@ namespace Barotrauma public float UpgradePreviewScale = 1.0f; + private IReadOnlyList wearableDamageModifiers; + private IReadOnlyDictionary wearableSkillModifiers; + //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] public Color InventoryIconColor { get; protected set; } @@ -101,6 +103,9 @@ namespace Barotrauma var containedSprites = new List(); var decorativeSpriteGroups = new Dictionary>(); + var wearableDamageModifiers = new List(); + var wearableSkillModifiers = new Dictionary(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) @@ -198,8 +203,33 @@ namespace Barotrauma containedSprites.Add(containedSprite); } break; + case "wearable": + foreach (ContentXElement wearableSubElement in subElement.Elements()) + { + switch (wearableSubElement.Name.LocalName.ToLowerInvariant()) + { + case "damagemodifier": + wearableDamageModifiers.Add(new DamageModifier(wearableSubElement, Name.Value + ", Wearable", checkErrors: false)); + break; + case "skillmodifier": + Identifier skillIdentifier = wearableSubElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); + float skillValue = wearableSubElement.GetAttributeFloat("skillvalue", 0f); + if (wearableSkillModifiers.ContainsKey(skillIdentifier)) + { + wearableSkillModifiers[skillIdentifier] += skillValue; + } + else + { + wearableSkillModifiers.TryAdd(skillIdentifier, skillValue); + } + break; + } + } + break; } } + this.wearableDamageModifiers = wearableDamageModifiers.ToImmutableList(); + this.wearableSkillModifiers = wearableSkillModifiers.ToImmutableDictionary(); UpgradeOverrideSprites = upgradeOverrideSprites.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); BrokenSprites = brokenSprites.ToImmutableArray(); @@ -208,6 +238,25 @@ namespace Barotrauma DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); } + public bool CanCharacterBuy() + { + if (!DefaultPrice.RequiresUnlock) { return true; } + return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this); + } + public LocalizedString GetTooltip() + { + LocalizedString tooltip = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{Name}‖color:end‖"; + if (!Description.IsNullOrEmpty()) + { + tooltip += $"\n{Description}"; + } + if (wearableDamageModifiers.Any() || wearableSkillModifiers.Any()) + { + Wearable.AddTooltipInfo(wearableDamageModifiers, wearableSkillModifiers, ref tooltip); + } + return tooltip; + } + public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); @@ -313,15 +362,7 @@ namespace Barotrauma } else { - Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); - Vector2 placeSize = Size * Scale; - if (placePosition != Vector2.Zero) - { - if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } - if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } - position = placePosition; - } - Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite.DrawTiled(spriteBatch, new Vector2(placeRect.X, -placeRect.Y), placeRect.Size.ToVector2(), SpriteColor * 0.8f); } } } 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/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index ce1348882..f5fc36a94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -226,7 +226,7 @@ namespace Barotrauma { if (SoundChannels[i] == null || !SoundChannels[i].IsPlaying) { - SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, soundPos); + SoundChannels[i] = roundSound.Sound.Play(roundSound.Volume, roundSound.Range, roundSound.GetRandomFrequencyMultiplier(), soundPos); } SoundChannels[i].Position = new Vector3(soundPos.X, soundPos.Y, 0.0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index bdf919218..c123c22f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -178,7 +179,7 @@ namespace Barotrauma activeSprite?.Draw( spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 3000.0f), + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, obj.CurrentScale, @@ -200,7 +201,7 @@ namespace Barotrauma obj.ActivePrefab.DeformableSprite.Origin, obj.CurrentRotation, obj.CurrentScale, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 5000.0f)); + Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f)); } 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/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index c32f82187..9902a8072 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 { @@ -98,7 +99,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 +187,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) { @@ -252,27 +253,104 @@ 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); + } + } + } } partial void ClearAnimQueue() @@ -280,18 +358,19 @@ 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) { RemoveFogOfWar(currentDisplayLocation); - currentDisplayLocation.Discover(); + Discover(currentDisplayLocation); if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = currentDisplayLocation; @@ -452,13 +531,13 @@ namespace Barotrauma Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); - CurrentLocation.Discover(); + Discover(CurrentLocation); OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation)); SelectLocation(-1); if (GameMain.Client == null) { CurrentLocation.CreateStores(); - ProgressWorld(); + ProgressWorld(campaign); Radiation?.OnStep(1); } else @@ -481,10 +560,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; @@ -568,7 +647,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; @@ -587,14 +668,32 @@ namespace Barotrauma } 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 +725,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 +737,18 @@ 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 +757,12 @@ namespace Barotrauma if (GameMain.DebugDraw) { Vector2 dPos = pos; - if (location == HighlightedLocation && (!location.Discovered || !location.HasOutpost()) && location.Reputation != null) + if (location == HighlightedLocation) { 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); + 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; } dPos.Y += 48; GUI.DrawString(spriteBatch, dPos, $"Difficulty: {location.LevelData.Difficulty.FormatSingleDecimal()}", Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); @@ -693,11 +788,27 @@ namespace Barotrauma 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 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); - bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; + Vector2 factionSize = HighlightedLocation.Faction == null ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Faction.Prefab.Name); + bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; + Vector2 descSize = HighlightedLocation.Type.Description.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.SmallFont.MeasureString(HighlightedLocation.Type.Description); + Vector2 size = new Vector2(Math.Max(factionSize.X, Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X))), nameSize.Y + factionSize.Y+ typeSize.Y + descSize.Y); + + int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); + var 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)); + } + } + size.Y += ((highestSubTier > 0 ? 1 : 0) + overrideTiers.Count) * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; + LocalizedString repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; - if (showReputation) + if (showReputation && HighlightedLocation.Reputation != null) { repLabelText = TextManager.Get("reputation"); repLabelSize = GUIStyle.Font.MeasureString(repLabelText); @@ -706,21 +817,59 @@ namespace Barotrauma 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); + spriteBatch, + new Rectangle( + (int)(pos.X - 60 * GUI.Scale), + (int)(pos.Y - size.Y), + (int)(size.X + 120 * GUI.Scale), + (int)(size.Y * 2.0f)), + 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); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + + if (!HighlightedLocation.Type.Name.IsNullOrEmpty()) + { + DrawText(HighlightedLocation.Type.Name); + topLeftPos += new Vector2(0.0f, typeSize.Y); + } + if (HighlightedLocation.Faction != null) + { + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Faction.Prefab.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + topLeftPos += new Vector2(0.0f, factionSize.Y); + } + if (!HighlightedLocation.Type.Description.IsNullOrEmpty()) + { + DrawText(HighlightedLocation.Type.Description, font: GUIStyle.SmallFont); + topLeftPos += new Vector2(0.0f, descSize.Y); + } + if (highestSubTier > 0) + { + DrawSubAvailabilityText("advancedsub.all", highestSubTier); + } + foreach (var (subClass, tier) in overrideTiers) + { + DrawSubAvailabilityText($"advancedsub.{subClass}", tier); + } + void DrawSubAvailabilityText(string tag, int tier) + { + DrawText(TextManager.GetWithVariable(tag, "[tiernumber]", tier.ToString()), font: GUIStyle.SmallFont); + topLeftPos += new Vector2(0.0f, typeSize.Y); + } if (showReputation) { - topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, repLabelText.Value, GUIStyle.TextColorNormal * hudVisibility * 1.5f); + //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) @@ -939,17 +1088,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; } @@ -1020,13 +1167,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 a33dfa34a..14aced877 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -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 /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index b41a0f0a6..061a2a37c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -295,6 +295,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/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 1d862e8a6..72eba95b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -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), @@ -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/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 76040fcd3..061191f03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -311,6 +311,12 @@ namespace Barotrauma.Networking CoroutineManager.StartCoroutine(WaitForStartingInfo(), "WaitForStartingInfo"); } + public void SetLobbyPublic(bool isPublic) + { + GameMain.NetLobbyScreen.SetPublic(isPublic); + SteamManager.SetLobbyPublic(isPublic); + } + private ClientPeer CreateNetPeer() { Networking.ClientPeer.Callbacks callbacks = new ClientPeer.Callbacks( @@ -1319,6 +1325,7 @@ namespace Barotrauma.Networking ServerSettings.MaximumMoneyTransferRequest = inc.ReadInt32(); bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); + ServerSettings.ShowEnemyHealthBars = (EnemyHealthBarMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; @@ -2521,7 +2528,7 @@ namespace Barotrauma.Networking public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { - if (!(entity is IClientSerializable clientSerializable)) + if (entity is not IClientSerializable clientSerializable) { throw new InvalidCastException($"Entity is not {nameof(IClientSerializable)}"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 4a46f3c9e..c6cce06c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -277,16 +277,12 @@ namespace Barotrauma.Networking public void Clear() { - ID = 0; - lastReceivedID = 0; - firstNewID = null; - - events.Clear(); eventLastSent.Clear(); - MidRoundSyncingDone = false; + + ClearSelf(); } /// @@ -297,6 +293,10 @@ namespace Barotrauma.Networking { ID = 0; events.Clear(); + if (thisClient != null) + { + thisClient.LastSentEntityEventID = 0; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index fc23c4df9..56138ec83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking { static class PingUtils { - private static readonly Dictionary activePings = new Dictionary(); + private static readonly Dictionary activePings = new Dictionary(); private static bool steamPingInfoReady; @@ -36,9 +36,9 @@ namespace Barotrauma.Networking switch (serverInfo.Endpoint) { - case LidgrenEndpoint { NetEndpoint: { Address: var address } }: + case LidgrenEndpoint { NetEndpoint: var endPoint }: - GetIPAddressPing(serverInfo, address, onPingDiscovered); + GetIPAddressPing(serverInfo, endPoint, onPingDiscovered); break; case SteamP2PEndpoint steamP2PEndpoint: TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})", @@ -131,9 +131,9 @@ namespace Barotrauma.Networking } } - private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action onPingDiscovered) + private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action onPingDiscovered) { - if (IPAddress.IsLoopback(address)) + if (IPAddress.IsLoopback(endPoint.Address)) { serverInfo.Ping = Option.Some(0); onPingDiscovered(serverInfo); @@ -142,24 +142,24 @@ namespace Barotrauma.Networking { lock (activePings) { - if (activePings.ContainsKey(address)) { return; } - activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0); + if (activePings.ContainsKey(endPoint)) { return; } + activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0); } serverInfo.Ping = Option.None(); - TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000), + TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000), rtt => { if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option.None(); } onPingDiscovered(serverInfo); lock (activePings) { - activePings.Remove(address); + activePings.Remove(endPoint); } }); } } - private static async Task> PingServerAsync(IPAddress ipAddress, int timeOut) + private static async Task> PingServerAsync(IPEndPoint endPoint, int timeOut) { await Task.Yield(); bool shouldGo = false; @@ -167,21 +167,21 @@ namespace Barotrauma.Networking { lock (activePings) { - shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25; + shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25; } await Task.Delay(25); } - if (ipAddress == null) { return Option.None(); } + if (endPoint?.Address == null) { return Option.None(); } //don't attempt to ping if the address is IPv6 and it's not supported - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } + if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.None(); } Ping ping = new Ping(); byte[] buffer = new byte[32]; try { - PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true)); + PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true)); return pingReply.Status switch { @@ -191,9 +191,9 @@ namespace Barotrauma.Networking } catch (Exception ex) { - GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); + GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message)); #if DEBUG - DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); + DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red); #endif return Option.None(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index b6c1b2583..c6a97de44 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -140,7 +140,7 @@ namespace Barotrauma.Networking MaxPlayers = incMsg.ReadByte(); HasPassword = incMsg.ReadBoolean(); IsPublic = incMsg.ReadBoolean(); - GameMain.NetLobbyScreen.SetPublic(IsPublic); + GameMain.Client?.SetLobbyPublic(IsPublic); AllowFileTransfers = incMsg.ReadBoolean(); incMsg.ReadPadBits(); TickRate = incMsg.ReadRangedInteger(1, 60); @@ -367,6 +367,17 @@ namespace Barotrauma.Networking //*********************************************** + //changing server visibility on the fly is not supported in dedicated servers + if (GameMain.Client?.ClientPeer is not LidgrenClientPeer) + { + var isPublic = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), + TextManager.Get("publicserver")) + { + ToolTip = TextManager.Get("publicservertooltip") + }; + GetPropertyData(nameof(IsPublic)).AssignGUIComponent(isPublic); + } + // Sub Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) @@ -475,9 +486,10 @@ namespace Barotrauma.Networking // game settings //-------------------------------------------------------------------------------- - var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)) { }; + var roundsTab = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.Center)); + var roundsContent = new GUIListBox(new RectTransform(Vector2.One, roundsTab.RectTransform, Anchor.Center), style: "GUIListBoxNoBorder").Content; - GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsTab.RectTransform)); + GUILayoutGroup playStyleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), roundsContent.RectTransform)); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playStyleLayout.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont); var playstyleList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), playStyleLayout.RectTransform)) @@ -502,7 +514,7 @@ namespace Barotrauma.Networking GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); playstyleList.RectTransform.MinSize = new Point(0, (int)(playstyleList.Content.Children.First().Rect.Height * 2.0f + playstyleList.Padding.Y + playstyleList.Padding.W)); - GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsTab.RectTransform)) + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), roundsContent.RectTransform)) { Stretch = true }; @@ -608,7 +620,7 @@ namespace Barotrauma.Networking }; slider.OnMoved(slider, slider.BarScroll); - GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsTab.RectTransform)); + GUILayoutGroup losModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); var losModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), losModeLayout.RectTransform), TextManager.Get("LosEffect")); @@ -629,7 +641,30 @@ namespace Barotrauma.Networking } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); - GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsTab.RectTransform)) + GUILayoutGroup healthBarModeLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.14f), roundsContent.RectTransform)); + + var healthBarModeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), healthBarModeLayout.RectTransform), + TextManager.Get("ShowEnemyHealthBars")); + + var healthBarModeRadioButtonLayout + = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), healthBarModeLayout.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + var healthBarModeRadioButtonGroup = new GUIRadioButtonGroup(); + EnemyHealthBarMode[] healthBarModeModes = Enum.GetValues(); + for (int i = 0; i < healthBarModeModes.Length; i++) + { + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), healthBarModeRadioButtonLayout.RectTransform), + TextManager.Get($"ShowEnemyHealthBars.{healthBarModeModes[i]}"), + font: GUIStyle.SmallFont, style: "GUIRadioButton"); + healthBarModeRadioButtonGroup.AddRadioButton(i, losTick); + } + GetPropertyData(nameof(ShowEnemyHealthBars)).AssignGUIComponent(healthBarModeRadioButtonGroup); + + GUILayoutGroup numberLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), roundsContent.RectTransform)) { Stretch = true }; @@ -651,7 +686,7 @@ namespace Barotrauma.Networking var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), numberLayout.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); GetPropertyData(nameof(DisableBotConversations)).AssignGUIComponent(disableBotConversationsBox); - GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsTab.RectTransform), isHorizontal: true) + GUILayoutGroup buttonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), roundsContent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index cae3906e3..55d0d15cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -82,6 +82,12 @@ namespace Barotrauma.Particles [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool CopyEntityAngle { get; set; } + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")] + public bool CopyEntityDir { get; set; } + + [Editable, Serialize(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; } @@ -200,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 5b09b97aa..23c90ff23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -59,6 +59,7 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.None: + if (Key == Keys.None) { return false; } return PlayerInput.KeyDown(Key); case MouseButton.PrimaryMouse: return PlayerInput.PrimaryMouseButtonHeld(); @@ -88,6 +89,7 @@ namespace Barotrauma switch (MouseButton) { case MouseButton.None: + if (Key == Keys.None) { return false; } return PlayerInput.KeyHit(Key); case MouseButton.PrimaryMouse: return PlayerInput.PrimaryMouseButtonClicked(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 436132dba..3da38f7df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -112,10 +112,23 @@ namespace Barotrauma } StringBuilder sb = new StringBuilder(); + sb.AppendLine("Barotrauma Client crash report (generated on " + DateTime.Now + ")"); - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); - sb.AppendLine("\n"); + sb.AppendLine(); + + string dxgiErrorHelpText = +#if WINDOWS + GetDXGIErrorHelpText(game, exception); +#else + string.Empty; +#endif + if (!string.IsNullOrEmpty(dxgiErrorHelpText)) + { + sb.AppendLine(dxgiErrorHelpText); + sb.AppendLine(); + } try { @@ -135,7 +148,7 @@ namespace Barotrauma XDocument newDoc = new XDocument(newElement); newDoc.Save(GameSettings.PlayerConfigPath); sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); - sb.AppendLine("\n"); + sb.AppendLine(); } } } @@ -148,7 +161,7 @@ namespace Barotrauma { sb.AppendLine(exeHash.StringRepresentation); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); sb.AppendLine($"Graphics mode: {GameSettings.CurrentConfig.Graphics.Width}x{GameSettings.CurrentConfig.Graphics.Height} ({GameSettings.CurrentConfig.Graphics.DisplayMode})"); @@ -171,7 +184,7 @@ namespace Barotrauma sb.AppendLine("Client (" + (GameMain.Client.GameStarted ? "Round had started)" : "Round hadn't been started)")); } - sb.AppendLine("\n"); + sb.AppendLine(); sb.AppendLine("System info:"); sb.AppendLine(" Operating system: " + System.Environment.OSVersion + (System.Environment.Is64BitOperatingSystem ? " 64 bit" : " x86")); @@ -201,13 +214,14 @@ namespace Barotrauma } } - sb.AppendLine("\n"); - sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")"); + sb.AppendLine(); + sb.AppendLine($"Exception: {exception.Message} ({exception.GetType()})"); #if WINDOWS if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) { var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; - sb.AppendLine("Device removed reason: " + dxDevice.DeviceRemovedReason.ToString()); + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + sb.AppendLine($"Device removed reason: {descriptor} ({dxDevice.DeviceRemovedReason})"); } #endif if (exception.TargetSite != null) @@ -219,7 +233,7 @@ namespace Barotrauma { sb.AppendLine("Stack trace: "); sb.AppendLine(exception.StackTrace.CleanupStackTrace()); - sb.AppendLine("\n"); + sb.AppendLine(); } if (exception.InnerException != null) @@ -260,18 +274,43 @@ namespace Barotrauma if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } - + + string msg = string.Empty; if (GameAnalyticsManager.SendUserStatistics) { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers."; } else { - CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + - " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/", filePath); + msg = "A crash report (\"" + filePath + "\") was saved in the root folder of the game. The error was not sent to the developers because user statistics have been disabled, but" + + " if you'd like to help fix this bug, you may post it on Barotrauma's GitHub issue tracker: https://github.com/Regalis11/Barotrauma/issues/"; } + if (string.IsNullOrEmpty(dxgiErrorHelpText)) + { + msg += "\n\n" + dxgiErrorHelpText; + } + CrashMessageBox(msg, filePath); } +#if WINDOWS + private static string GetDXGIErrorHelpText(GameMain game, Exception exception) + { + string text = string.Empty; + if (exception is SharpDXException sharpDxException && ((uint)sharpDxException.HResult) == 0x887A0005) + { + var dxDevice = (SharpDX.Direct3D11.Device)game.GraphicsDevice.Handle; + var descriptor = ResultDescriptor.Find(dxDevice.DeviceRemovedReason)?.ApiCode ?? "UNKNOWN"; + + text += + $"The crash was caused by the DirectX error {descriptor} ({dxDevice.DeviceRemovedReason}). " + + "This is a common DirectX error that can be related to various different issues, such as outdated drivers, RAM problems or an overclocked or otherwise overstressed GPU. " + + "There are several potential ways to fix the issue: ensuring your graphics drivers and DirectX installation are up-to-date, disabling overclocking and adjusting various GPU-specific settings. " + + $"You may also be able to find potential solutions to the problem by using the error code {descriptor} ({dxDevice.DeviceRemovedReason}) and your GPU manufacturer as search terms."; + } + return text; + } +#endif + private static IntPtr nvApi64Dll = IntPtr.Zero; private static void EnableNvOptimus() { @@ -287,11 +326,11 @@ namespace Barotrauma private static void FreeNvOptimus() { - #warning TODO: determine if we can do this safely +#warning TODO: determine if we can do this safely //NativeLibrary.Free(nvApi64Dll); } } #endif - + } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index 17e268d1e..56dce764f 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() { @@ -42,13 +38,10 @@ namespace Barotrauma 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); @@ -56,14 +49,13 @@ namespace Barotrauma 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 +65,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 46dfef1ef..5083a49c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -104,6 +104,7 @@ namespace Barotrauma public struct CampaignSettingElements { + public SettingValue TutorialEnabled; public SettingValue RadiationEnabled; public SettingValue MaxMissionCount; public SettingValue StartingFunds; @@ -114,6 +115,7 @@ namespace Barotrauma { return new CampaignSettings(element: null) { + TutorialEnabled = TutorialEnabled.GetValue(), RadiationEnabled = RadiationEnabled.GetValue(), MaxMissionCount = MaxMissionCount.GetValue(), StartingBalanceAmount = StartingFunds.GetValue(), @@ -159,7 +161,7 @@ namespace Barotrauma } } - protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings) + protected static CampaignSettingElements CreateCampaignSettingList(GUIComponent parent, CampaignSettings prevSettings, bool isSinglePlayer) { const float verticalSize = 0.14f; @@ -180,6 +182,9 @@ namespace Barotrauma 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); ImmutableArray> startingSetOptions = StartItemSet.Sets.OrderBy(s => s.Order).Select(set => new SettingCarouselElement(set.Identifier, $"startitemset.{set.Identifier}")).ToImmutableArray(); @@ -214,6 +219,7 @@ namespace Barotrauma { if (o is CampaignSettings settings) { + tutorialEnabled.SetValue(isSinglePlayer && settings.TutorialEnabled); radiationEnabled.SetValue(settings.RadiationEnabled); maxMissionCountInput.SetValue(settings.MaxMissionCount); startingFundsInput.SetValue(settings.StartingBalanceAmount); @@ -226,6 +232,7 @@ namespace Barotrauma return new CampaignSettingElements { + TutorialEnabled = tutorialEnabled, RadiationEnabled = radiationEnabled, MaxMissionCount = maxMissionCountInput, StartingFunds = startingFundsInput, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 70a8bbd8c..66f0877d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -46,7 +46,7 @@ namespace Barotrauma nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingLayout, CampaignSettings.Empty, false); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), verticalLayout.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 5688ea044..1b8f74e3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -370,7 +370,7 @@ namespace Barotrauma GUILayoutGroup campaignSettingContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.8f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)); - CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings); + CampaignSettingElements elements = CreateCampaignSettingList(campaignSettingContent, prevSettings, true); CampaignCustomizeSettings.Buttons[0].OnClicked += (button, o) => { @@ -608,15 +608,16 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { + var saveFolder = SaveUtil.GetSaveFolder(SaveUtil.SaveType.Singleplayer); try { - ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); + ToolBox.OpenFileWithShell(saveFolder); } catch (Exception e) { new GUIMessageBox( - TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", ("[folder]", SaveUtil.SaveFolder), ("[errormessage]", e.Message))); + TextManager.Get("error"), + TextManager.GetWithVariables("showinfoldererror", ("[folder]", saveFolder), ("[errormessage]", e.Message))); } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 2b5510ab7..d30dee7fa 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); 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); @@ -426,8 +475,10 @@ 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); - + 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)); @@ -472,7 +523,12 @@ namespace Barotrauma { TextGetter = () => { - return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}"); + int missionCount = 0; + if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null) + { + missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m)); + } + return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}"); } }; @@ -482,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) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 668226715..bc78e1fe5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -39,6 +39,7 @@ namespace Barotrauma.CharacterEditor private bool ShowExtraRagdollControls => editLimbs || editJoints; + public Character SpawnedCharacter => character; private Character character; private Vector2 spawnPosition; @@ -997,7 +998,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(); @@ -1513,7 +1514,7 @@ namespace Barotrauma.CharacterEditor } } - private Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) + public Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) { DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); @@ -3181,10 +3182,7 @@ namespace Barotrauma.CharacterEditor OnClicked = (button, data) => { ResetView(); - CharacterParams.Serialize(); - RagdollParams.Serialize(); - AnimParams.ForEach(a => a.Serialize()); - Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + PrepareCharacterCopy(); Wizard.Instance.SelectTab(Wizard.Tab.Character); return true; } @@ -3209,9 +3207,17 @@ namespace Barotrauma.CharacterEditor fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f)); } -#endregion + #endregion -#region ToggleButtons + public void PrepareCharacterCopy() + { + CharacterParams.Serialize(); + RagdollParams.Serialize(); + AnimParams.ForEach(a => a.Serialize()); + Wizard.Instance.CopyExisting(CharacterParams, RagdollParams, AnimParams); + } + + #region ToggleButtons private enum Direction { Left, @@ -4234,7 +4240,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; @@ -5007,9 +5013,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/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 642648f1b..64a6b65c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -101,7 +101,7 @@ namespace Barotrauma.CharacterEditor { bool isSamePackage = contentPackage.GetFiles().Any(f => Path.GetFileNameWithoutExtension(f.Path.Value) == name); LocalizedString verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); - var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }, type: GUIMessageBox.Type.Warning) { UserData = "verificationprompt" }; @@ -356,7 +356,7 @@ namespace Barotrauma.CharacterEditor } if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken"), type: GUIMessageBox.Type.Warning); return false; } string modName = contentPackageNameElement.Text; @@ -428,17 +428,26 @@ namespace Barotrauma.CharacterEditor texturePathElement.Flash(useRectangleFlash: true); return false; } + if (Name == CharacterPrefab.HumanSpeciesName && !IsCopy) + { + // Force a copy when trying to override a human, because handling the crash would be very difficult (we require humans to have certain definitions). + if (!CharacterEditorScreen.Instance.SpawnedCharacter.IsHuman) + { + CharacterEditorScreen.Instance.SpawnCharacter(CharacterPrefab.HumanSpeciesName); + } + CharacterEditorScreen.Instance.PrepareCharacterCopy(); + } if (IsCopy) { SourceRagdoll.Texture = evaluatedTexturePath; SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; SourceRagdoll.CanWalk = CanWalk; SourceRagdoll.Serialize(); - Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); + Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); } else { - Wizard.Instance.SelectTab(Tab.Ragdoll); + Instance.SelectTab(Tab.Ragdoll); } return true; }; @@ -470,9 +479,6 @@ namespace Barotrauma.CharacterEditor Stretch = true, RelativeSpacing = 0.02f }; - // HTML - GUIMessageBox htmlBox = null; - var loadHtmlButton = new GUIButton(new RectTransform(new Point(content.Rect.Width / 3, elementSize), content.RectTransform), GetCharacterEditorTranslation("LoadFromHTML")); // Limbs var limbsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; @@ -689,69 +695,6 @@ namespace Barotrauma.CharacterEditor return true; } }; - loadHtmlButton.OnClicked = (b, d) => - { - if (htmlBox == null) - { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new LocalizedString[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); - htmlBox.Header.Font = GUIStyle.LargeFont; - var element = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); - //new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); - var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath").Value); - LocalizedString title = GetCharacterEditorTranslation("SelectFile"); - new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) - { - OnClicked = (button, data) => - { - FileSelection.OnFileSelected = (file) => - { - htmlPathElement.Text = file; - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("HTML", "*.html, *.htm"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.html, *.htm"); - FileSelection.Open = true; - return true; - } - }; - var list = new GUIListBox(new RectTransform(new Vector2(1, 0.8f), htmlBox.Content.RectTransform)); - var htmlOutput = new GUITextBlock(new RectTransform(Vector2.One, list.Content.RectTransform), string.Empty) { CanBeFocused = false }; - htmlBox.Buttons[0].OnClicked += (_b, _d) => - { - htmlBox.Close(); - return true; - }; - htmlBox.Buttons[1].OnClicked += (_b, _d) => - { - LimbGUIElements.ForEach(l => l.RectTransform.Parent = null); - LimbGUIElements.Clear(); - JointGUIElements.ForEach(j => j.RectTransform.Parent = null); - JointGUIElements.Clear(); - LimbXElements.Clear(); - JointXElements.Clear(); - ParseRagdollFromHTML(htmlPathElement.Text, (id, limbName, limbType, rect) => - { - CreateLimbGUIElement(limbsList.Content.RectTransform, elementSize, id, limbName, limbType, rect); - }, (id1, id2, anchor1, anchor2, jointName) => - { - CreateJointGUIElement(jointsList.Content.RectTransform, elementSize, id1, id2, anchor1, anchor2, jointName); - }); - htmlOutput.Text = new XDocument(new XElement("Ragdoll", new object[] - { - new XAttribute("type", Name), LimbXElements.Values, JointXElements - })).ToString(); - htmlOutput.CalculateHeightFromText(); - list.UpdateScrollBarSize(); - return true; - }; - } - else - { - GUIMessageBox.MessageBoxes.Add(htmlBox); - } - return true; - }; // Previous box.Buttons[0].OnClicked += (b, d) => { @@ -1070,7 +1013,6 @@ namespace Barotrauma.CharacterEditor // Rectangles colliderAttributes.Add(new XAttribute("height", (int)(height * 0.85f))); colliderAttributes.Add(new XAttribute("width", (int)(width * 0.85f))); - idToCodeName.TryGetValue(id, out string notes); LimbXElements.Add(id.ToString(), new XElement("limb", new XAttribute("id", id), new XAttribute("name", limbName), @@ -1107,188 +1049,6 @@ namespace Barotrauma.CharacterEditor } } - Dictionary idToCodeName = new Dictionary(); - protected void ParseRagdollFromHTML(string path, Action limbCallback = null, Action jointCallback = null) - { - // TODO: parse as xml files -> allows to load ragdolls onto the wizard. - //XDocument doc = XMLExtensions.TryLoadXml(path); - //var xElements = doc.Elements().ToArray(); - string html = string.Empty; - try - { - html = File.ReadAllText(path); - } - catch (Exception e) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("FailedToReadHTML").Replace("[path]", path), e); - return; - } - - var lines = html.Split(new string[] { "", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.Contains("left") && s.Contains("top") && s.Contains("width") && s.Contains("height")); - int id = 0; - Dictionary hierarchyToID = new Dictionary(); - Dictionary idToHierarchy = new Dictionary(); - Dictionary idToPositionCode = new Dictionary(); - Dictionary idToName = new Dictionary(); - idToCodeName.Clear(); - foreach (var line in lines) - { - var codeNames = new string(line.SkipWhile(c => c != '>').Skip(1).ToArray()).Split(','); - for (int i = 0; i < codeNames.Length; i++) - { - string codeName = codeNames[i].Trim(); - if (string.IsNullOrWhiteSpace(codeName)) { continue; } - idToCodeName.Add(id, codeName); - string limbName = new string(codeName.SkipWhile(c => c != '_').Skip(1).ToArray()); - if (string.IsNullOrWhiteSpace(limbName)) { continue; } - idToName.Add(id, limbName); - var parts = line.Split(' '); - int ParseToInt(string selector) - { - string part = parts.First(p => p.Contains(selector)); - string s = new string(part.SkipWhile(c => c != ':').Skip(1).TakeWhile(c => char.IsNumber(c)).ToArray()); - int.TryParse(s, out int v); - return v; - }; - // example: 111311cr -> 111311 - string hierarchy = new string(codeName.TakeWhile(c => char.IsNumber(c)).ToArray()); - if (hierarchyToID.ContainsKey(hierarchy)) - { - DebugConsole.ThrowError(GetCharacterEditorTranslation("MultipleItemsWithSameHierarchy").Replace("[hierarchy]", hierarchy).Replace("[name]", codeName)); - return; - } - hierarchyToID.Add(hierarchy, id); - idToHierarchy.Add(id, hierarchy); - string positionCode = new string(codeName.SkipWhile(c => char.IsNumber(c)).TakeWhile(c => c != '_').ToArray()); - idToPositionCode.Add(id, positionCode.ToLowerInvariant()); - int x = ParseToInt("left"); - int y = ParseToInt("top"); - int width = ParseToInt("width"); - int height = ParseToInt("height"); - // This is overridden when the data is loaded from the gui fields. - LimbXElements.Add(hierarchy, new XElement("limb", - new XAttribute("id", id), - new XAttribute("name", limbName), - new XAttribute("type", ParseLimbType(limbName).ToString()), - new XElement("sprite", - new XAttribute("texture", ""), - new XAttribute("sourcerect", $"{x}, {y}, {width}, {height}")) - )); - limbCallback?.Invoke(id, limbName, ParseLimbType(limbName), new Rectangle(x, y, width, height)); - id++; - } - } - for (int i = 0; i < id; i++) - { - if (idToHierarchy.TryGetValue(i, out string hierarchy)) - { - if (hierarchy != "0") - { - // NEW LOGIC: if hierarchy length == 1, parent to 0 - // Else parent to the last bone in the current hierarchy (11 is parented to 1, 212 is parented to 21 etc) - string parent = hierarchy.Length > 1 ? hierarchy.Remove(hierarchy.Length - 1, 1) : "0"; - if (hierarchyToID.TryGetValue(parent, out int parentID)) - { - Vector2 anchor1 = Vector2.Zero; - Vector2 anchor2 = Vector2.Zero; - idToName.TryGetValue(parentID, out string parentName); - idToName.TryGetValue(i, out string limbName); - string jointName = $"{GetCharacterEditorTranslation("Joint")} {parentName} - {limbName}"; - if (idToPositionCode.TryGetValue(i, out string positionCode)) - { - float scalar = 0.8f; - if (LimbXElements.TryGetValue(parent, out XElement parentElement)) - { - Rectangle parentSourceRect = parentElement.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float parentWidth = parentSourceRect.Width / 2 * scalar; - float parentHeight = parentSourceRect.Height / 2 * scalar; - switch (positionCode) - { - case "tl": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "tc": // 0, 1 - anchor1 = new Vector2(0, parentHeight); - break; - case "tr": // -1, 1 - anchor1 = new Vector2(-parentWidth, parentHeight); - break; - case "cl": // -1, 0 - anchor1 = new Vector2(-parentWidth, 0); - break; - case "cr": // 1, 0 - anchor1 = new Vector2(parentWidth, 0); - break; - case "bl": // -1, -1 - anchor1 = new Vector2(-parentWidth, -parentHeight); - break; - case "bc": // 0, -1 - anchor1 = new Vector2(0, -parentHeight); - break; - case "br": // 1, -1 - anchor1 = new Vector2(parentWidth, -parentHeight); - break; - } - if (LimbXElements.TryGetValue(hierarchy, out XElement element)) - { - Rectangle sourceRect = element.Element("sprite").GetAttributeRect("sourcerect", Rectangle.Empty); - float width = sourceRect.Width / 2 * scalar; - float height = sourceRect.Height / 2 * scalar; - switch (positionCode) - { - // Inverse - case "tl": - // br - anchor2 = new Vector2(-width, -height); - break; - case "tc": - // bc - anchor2 = new Vector2(0, -height); - break; - case "tr": - // bl - anchor2 = new Vector2(-width, -height); - break; - case "cl": - // cr - anchor2 = new Vector2(width, 0); - break; - case "cr": - // cl - anchor2 = new Vector2(-width, 0); - break; - case "bl": - // tr - anchor2 = new Vector2(-width, height); - break; - case "bc": - // tc - anchor2 = new Vector2(0, height); - break; - case "br": - // tl - anchor2 = new Vector2(-width, height); - break; - } - } - } - } - // This is overridden when the data is loaded from the gui fields. - JointXElements.Add(new XElement("joint", - new XAttribute("name", jointName), - new XAttribute("limb1", parentID), - new XAttribute("limb2", i), - new XAttribute("limb1anchor", $"{anchor1.X.Format(2)}, {anchor1.Y.Format(2)}"), - new XAttribute("limb2anchor", $"{anchor2.X.Format(2)}, {anchor2.Y.Format(2)}") - )); - jointCallback?.Invoke(parentID, i, anchor1, anchor2, jointName); - } - } - } - } - } - protected LimbType ParseLimbType(string limbName) { var limbType = LimbType.None; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index e191d3e54..4a3916717 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 { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 38202f624..66ed3c9fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -16,8 +16,8 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = true; Hull.EditFire = false; Hull.EditWater = false; -#endif HumanAIController.DisableCrewAI = false; +#endif } protected virtual void DeselectEditorSpecific() { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 90f0e3f1b..02e03cbc1 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.Content; using Microsoft.Xna.Framework.Graphics; @@ -17,9 +18,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; @@ -115,13 +116,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; } - }); + } } } @@ -197,6 +198,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); @@ -204,7 +209,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(); @@ -231,7 +236,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 @@ -457,6 +466,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 420328bc7..68fe0d1b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -269,7 +269,7 @@ namespace Barotrauma }; #if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("settingstab.mods"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = ForceUpperCase.Yes, Enabled = true, @@ -334,7 +334,7 @@ namespace Barotrauma OnClicked = (button, userData) => { string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); + GameMain.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); return true; } }; @@ -463,13 +463,17 @@ namespace Barotrauma } }; var tutorialPreview = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), tutorialContent.RectTransform)) { RelativeSpacing = 0.05f, Stretch = true }; - var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), tutorialPreview.RectTransform), style: "InnerFrame"); + var imageContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "InnerFrame"); tutorialBanner = new GUIImage(new RectTransform(Vector2.One, imageContainer.RectTransform), style: null, scaleToFit: true); - var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); - var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); + var infoContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), tutorialPreview.RectTransform), style: "GUIFrameListBox"); + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoContainer.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) + { + AbsoluteSpacing = GUI.IntScale(10) + }; - tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.75f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); + tutorialHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont); + tutorialDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), string.Empty, wrap: true); var startButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.0f), infoContent.RectTransform, Anchor.BottomRight), text: TextManager.Get("startgamebutton")) { @@ -500,6 +504,10 @@ namespace Barotrauma private void SelectTutorial(Tutorial tutorial) { tutorialHeader.Text = tutorial.DisplayName; + tutorialHeader.CalculateHeightFromText(); + tutorialDescription.Text = tutorial.Description; + tutorialDescription.CalculateHeightFromText(); + (tutorialDescription.Parent as GUILayoutGroup)?.Recalculate(); tutorial.TutorialPrefab.Banner?.EnsureLazyLoaded(); tutorialBanner.Sprite = tutorial.TutorialPrefab.Banner; tutorialBanner.Color = tutorial.TutorialPrefab.Banner == null ? Color.Black : Color.White; @@ -945,25 +953,19 @@ namespace Barotrauma if (backgroundSprite == null) { -#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); + GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); } var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); 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(); + new Vector2(Math.Min(GameMain.GraphicsWidth / vignette.size.X, GameMain.GraphicsHeight / vignette.size.Y))); } } @@ -976,10 +978,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) @@ -1011,7 +1013,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f); if (mouseOn && PlayerInput.PrimaryMouseButtonClicked()) { - GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); + GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com"); } } textPos.Y -= textSize.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index a37fda6f5..34d6e7466 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -370,12 +370,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 6d966c50e..c7857e3e8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2741,11 +2741,8 @@ namespace Barotrauma public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { 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(); } 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 dcb6ab842..04a5eb0a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -1636,12 +1636,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(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs new file mode 100644 index 000000000..3d4b328ee --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SlideshowPlayer.cs @@ -0,0 +1,172 @@ +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 ?? "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) + { + GUI.DrawBackgroundSprite(spriteBatch, sprite, color); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index df8fc28ba..f32050a63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -379,6 +379,9 @@ namespace Barotrauma void CreateSprite(ContentXElement element) { + //empty element, probably an item variant? + if (element.Attributes().None()) { return; } + string spriteFolder = ""; ContentPath texturePath = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index aa6c37369..a6e6dc77f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -208,7 +208,7 @@ namespace Barotrauma private GUIFrame wiringToolPanel; - private DateTime editorSelectedTime; + private Option editorSelectedTime; private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; @@ -1391,7 +1391,7 @@ namespace Barotrauma if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - editorSelectedTime = DateTime.Now; + editorSelectedTime = Option.Some(DateTime.Now); GUI.ForceMouseOn(null); SetMode(Mode.Default); @@ -1540,9 +1540,13 @@ namespace Barotrauma autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); autoSaveLabel = null; - TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + if (editorSelectedTime.TryUnwrap(out DateTime selectedTime)) + { + TimeSpan timeInEditor = DateTime.Now - selectedTime; + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); + editorSelectedTime = Option.None(); + } #endif GUI.ForceMouseOn(null); @@ -2407,6 +2411,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)); //------------------------------------------------------------------ @@ -2487,7 +2502,7 @@ namespace Barotrauma { IntValue = MainSub.Info.Tier, MinValueInt = 1, - MaxValueInt = 3, + MaxValueInt = SubmarineInfo.HighestTier, OnValueChanged = (numberInput) => { MainSub.Info.Tier = numberInput.IntValue; @@ -2495,7 +2510,7 @@ namespace Barotrauma }; if (MainSub?.Info != null) { - MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, 3); + MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, SubmarineInfo.HighestTier); } var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -3228,7 +3243,7 @@ namespace Barotrauma } string pathWithoutUserName = Path.GetFullPath(sub.FilePath); - string saveFolder = Path.GetFullPath(SaveUtil.SaveFolder); + string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder); if (pathWithoutUserName.StartsWith(saveFolder)) { pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..]; @@ -4217,7 +4232,8 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, - OnSelected = SelectWire + OnSelected = SelectWire, + CanTakeKeyBoardFocus = false }; List wirePrefabs = new List(); @@ -5866,7 +5882,7 @@ namespace Barotrauma decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); Vector2 offset = new Vector2(GUI.IntScale(24)); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance} m", GUIStyle.TextColorNormal, font: GUIStyle.Font, backgroundColor: Color.Black, backgroundPadding: 4); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index abfbb86e2..fb7cc1008 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -21,6 +21,7 @@ namespace Barotrauma public static Character? dummyCharacter; public static Effect? BlueprintEffect; + public TabMenu? TabMenu; public TestScreen() { @@ -49,9 +50,10 @@ namespace Barotrauma } dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced)); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "captain")); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); + dummyCharacter.Info.GiveExperience(999999); miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false); @@ -61,6 +63,7 @@ namespace Barotrauma } Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); + TabMenu = new TabMenu(); } public override void AddToGUIUpdateList() @@ -68,35 +71,37 @@ namespace Barotrauma Frame.AddToGUIUpdateList(); CharacterHUD.AddToGUIUpdateList(dummyCharacter); dummyCharacter?.SelectedItem?.AddToGUIUpdateList(); + TabMenu?.AddToGUIUpdateList(); } public override void Update(double deltaTime) { base.Update(deltaTime); + TabMenu?.Update((float)deltaTime); - if (dummyCharacter is { } dummy && miniMapItem is { } item) - { - if (dummy.SelectedItem != item) - { - dummy.SelectedItem = item; - } - - dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime); - Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); - - foreach (Limb limb in dummy.AnimController.Limbs) - { - limb.body.SetTransform(pos, 0.0f); - } - - if (dummy.AnimController?.Collider is { } collider) - { - collider.SetTransform(pos, 0); - } - - dummy.ControlLocalPlayer((float)deltaTime, Cam, false); - dummy.Control((float)deltaTime, Cam); - } + // if (dummyCharacter is { } dummy && miniMapItem is { } item) + // { + // if (dummy.SelectedConstruction != item) + // { + // dummy.SelectedConstruction = item; + // } + // + // dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime); + // Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position); + // + // foreach (Limb limb in dummy.AnimController.Limbs) + // { + // limb.body.SetTransform(pos, 0.0f); + // } + // + // if (dummy.AnimController?.Collider is { } collider) + // { + // collider.SetTransform(pos, 0); + // } + // + // dummy.ControlLocalPlayer((float)deltaTime, Cam, false); + // dummy.Control((float)deltaTime, Cam); + // } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index a63e7c905..c3c4c3873 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -1332,16 +1332,18 @@ namespace Barotrauma } } - private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) + private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) { - if (entity is ItemComponent e) + if (GameMain.Client != null) { - entity = e.Item; - } - - if (GameMain.Client != null && entity is Item item) - { - GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property)); + if (entity is Item item) + { + GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property, item)); + } + else if (entity is ItemComponent ic) + { + GameMain.Client.CreateEntityEvent(ic.Item, new Item.ChangePropertyEventData(property, ic)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 65a6e87bc..594e36ad4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -176,7 +176,7 @@ namespace Barotrauma Action setter) where T : Enum => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); - private static void Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + private static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { var dropdown = new GUIDropDown(NewItemRectT(parent)); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); @@ -189,9 +189,10 @@ namespace Barotrauma setter((T)obj); return true; }; + return dropdown; } - private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) + private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") @@ -213,11 +214,12 @@ namespace Barotrauma setter(sb.BarScrollValue); return true; }; + return (slider, label); } - private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) { - var tickbox = new GUITickBox(NewItemRectT(parent), label) + return new GUITickBox(NewItemRectT(parent), label) { Selected = currentValue, ToolTip = tooltip, @@ -231,7 +233,7 @@ namespace Barotrauma private string Percentage(float v) => ToolBox.GetFormattedPercentage(v); - private int Round(float v) => (int)MathF.Round(v); + private static int Round(float v) => MathUtils.RoundToInt(v); private void CreateGraphicsTab() { @@ -262,30 +264,30 @@ namespace Barotrauma Spacer(left); Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); - DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, (v) => unsavedConfig.Graphics.DisplayMode = v); + DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v); Spacer(left); - Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, (v) => unsavedConfig.Graphics.VSync = v); - Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, (v) => unsavedConfig.Graphics.CompressTextures = v); + Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); + Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); - DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, (v) => unsavedConfig.Graphics.LosMode = v); + DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); Spacer(right); Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); - Slider(right, (0.5f, 1.0f), 11, (v) => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, (v) => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); + Slider(right, (0.5f, 1.0f), 11, v => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, v => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); - Slider(right, (10, 210), 21, (v) => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, - (v) => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); + Slider(right, (10, 210), 21, v => v > 200 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, + v => unsavedConfig.Graphics.VisibleLightLimit = v > 200 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); Spacer(right); - Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, (v) => unsavedConfig.Graphics.RadialDistortion = v); - Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, (v) => unsavedConfig.Graphics.ChromaticAberration = v); + Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v); + Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, v => unsavedConfig.Graphics.ChromaticAberration = v); Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); - Slider(right, (100, 1500), 15, (v) => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, (v) => unsavedConfig.Graphics.ParticleLimit = Round(v)); + Slider(right, (100, 1500), 15, v => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, v => unsavedConfig.Graphics.ParticleLimit = Round(v)); Spacer(right); } @@ -399,23 +401,23 @@ namespace Barotrauma Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, (v) => unsavedConfig.Audio.SoundVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => unsavedConfig.Audio.SoundVolume = v); Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => unsavedConfig.Audio.MusicVolume = v); Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, (v) => unsavedConfig.Audio.UiVolume = v); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => unsavedConfig.Audio.UiVolume = v); - Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); - Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v); + Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); - Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, (v) => unsavedConfig.Audio.VoiceChatVolume = v); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => unsavedConfig.Audio.VoiceChatVolume = v); - Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, (v) => unsavedConfig.Audio.UseDirectionalVoiceChat = v); - Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, (v) => unsavedConfig.Audio.VoipAttenuationEnabled = v); + Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v); + Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v); Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); @@ -424,7 +426,7 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); - DropdownEnum(voiceChat, (v) => TextManager.Get($"VoiceMode.{v}"), (v) => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, (v) => unsavedConfig.Audio.VoiceSetting = v); + DropdownEnum(voiceChat, v => TextManager.Get($"VoiceMode.{v}"), v => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, v => unsavedConfig.Audio.VoiceSetting = v); Spacer(voiceChat); var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); @@ -464,11 +466,11 @@ namespace Barotrauma Spacer(voiceChat); Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, (v) => unsavedConfig.Audio.MicrophoneVolume = v); + Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, v => unsavedConfig.Audio.MicrophoneVolume = v); Spacer(voiceChat); Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); - Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); + Slider(voiceChat, (0, 500), 26, v => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, v => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } private readonly Dictionary> inputButtonValueNameGetters = new Dictionary>(); @@ -481,8 +483,10 @@ namespace Barotrauma GUILayoutGroup layout = CreateCenterLayout(content); Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); - Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, (v) => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); - Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, (v) => unsavedConfig.EnableMouseLook = v); + + var aimAssistSlider = Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, v => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); + Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, v => unsavedConfig.EnableMouseLook = v); + Spacer(layout); GUIListBox keyMapList = @@ -523,7 +527,7 @@ namespace Barotrauma if (willBeSelected) { inputBoxSelectedThisFrame = true; - currentSetter = (v) => + currentSetter = v => { valueSetter(v); btn.Text = valueNameGetter(); @@ -626,7 +630,7 @@ namespace Barotrauma currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, - (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), + v => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), LegacyInputTypes.Contains(input)); } } @@ -644,7 +648,7 @@ namespace Barotrauma currRow, TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, - (v) => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); + v => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } @@ -663,6 +667,8 @@ namespace Barotrauma { unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); + aimAssistSlider.slider.BarScrollValue = GameSettings.Config.DefaultAimAssist; + aimAssistSlider.label.Text = Percentage(GameSettings.Config.DefaultAimAssist); foreach (var btn in inputButtonValueNameGetters.Keys) { btn.Text = inputButtonValueNameGetters[btn](); @@ -683,13 +689,13 @@ namespace Barotrauma .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); - Dropdown(layout, (v) => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, (v) => unsavedConfig.Language = v); + Dropdown(layout, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); Spacer(layout); - Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, (v) => unsavedConfig.PauseOnFocusLost = v); + Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); Spacer(layout); - Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, (v) => unsavedConfig.DisableInGameHints = v); + Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") @@ -710,13 +716,17 @@ namespace Barotrauma } }; Spacer(layout); - + + Label(layout, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); + DropdownEnum(layout, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); + Spacer(layout); + Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, (v) => unsavedConfig.Graphics.HUDScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, (v) => unsavedConfig.Graphics.InventoryScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); - Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, (v) => unsavedConfig.Graphics.TextScale = v); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); #if !OSX Spacer(layout); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 5870847e0..e073add2a 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; } @@ -564,11 +568,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) { @@ -755,6 +769,11 @@ namespace Barotrauma } } + if (Level.Loaded is { IsEndBiome: true }) + { + return "endlevel".ToIdentifier(); + } + Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { @@ -766,8 +785,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/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 1f59cf11e..41be59e49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -21,6 +21,13 @@ namespace Barotrauma private Entity soundEmitter; private double loopStartTime; private bool loopSound; + /// + /// Each new sound overrides the existing sounds that were launched with this status effect, meaning the old sound will be faded out and disposed and the new sound will be played instead of the old. + /// Normally the call to play the sound is ignored if there's an existing sound playing when the effect triggers. + /// Used for example for ensuring that rapid playing sounds restart playing even when the previous clip(s) have not yet stopped. + /// Use with caution. + /// + private bool forcePlaySounds; partial void InitProjSpecific(ContentXElement element, string parentDebugName) { @@ -50,6 +57,7 @@ namespace Barotrauma break; } } + forcePlaySounds = element.GetAttributeBool(nameof(forcePlaySounds), false); } partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull hull, Vector2 worldPosition, bool playSound) @@ -64,31 +72,36 @@ 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) { angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); particleRotation = -item.body.Rotation; - if (item.body.Dir < 0.0f) + if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f) { 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; + } } } } @@ -98,26 +111,35 @@ namespace Barotrauma particleRotation = -targetLimb.body.Rotation; float offset = targetLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; particleRotation += offset; - if (targetLimb.body.Dir < 0.0f) + 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; private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) { - if (sounds.Count == 0) return; + if (sounds.Count == 0) { return; } - if (soundChannel == null || !soundChannel.IsPlaying) + if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds) { + if (soundChannel != null && soundChannel.IsPlaying) + { + soundChannel.FadeOutAndDispose(); + } if (soundSelectionMode == SoundSelectionMode.All) { foreach (RoundSound sound in sounds) @@ -128,7 +150,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling); + soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling, freqMult: sound.GetRandomFrequencyMultiplier()); ignoreMuffling = sound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } @@ -155,7 +177,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier()); ignoreMuffling = selectedSound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index 54cab99e7..258594f3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -50,20 +50,25 @@ namespace Barotrauma.Steam lobbyState = LobbyState.Owner; lobbyID = (currentLobby?.Id).Value; - if (serverSettings.IsPublic) - { - currentLobby?.SetPublic(); - } - else - { - currentLobby?.SetFriendsOnly(); - } + SetLobbyPublic(serverSettings.IsPublic); currentLobby?.SetJoinable(true); UpdateLobby(serverSettings); }); } + public static void SetLobbyPublic(bool isPublic) + { + if (isPublic) + { + currentLobby?.SetPublic(); + } + else + { + currentLobby?.SetFriendsOnly(); + } + } + public static void UpdateLobby(ServerSettings serverSettings) { if (GameMain.Client == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index cce1627d8..afb6f8bd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -93,7 +93,7 @@ namespace Barotrauma.Steam return (left, center, right); } - private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) + private static void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to) { if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) { @@ -197,7 +197,11 @@ namespace Barotrauma.Steam out onInstalledInfoButtonHit, out var deselect); GUILayoutGroup mainLayout = - new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) + }; mainLayout.RectTransform.SetAsFirstChild(); var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f); @@ -257,7 +261,12 @@ namespace Barotrauma.Steam right.ChildAnchor = Anchor.TopRight; //enabled mods - Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var label = Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + new GUIImage(new RectTransform(new Point(label.Rect.Height), label.RectTransform, Anchor.CenterRight), style: "GUIButtonInfo") + { + ToolTip = TextManager.Get("ModLoadOrderExplanation") + }; + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) { CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, @@ -478,7 +487,7 @@ namespace Barotrauma.Steam { string str = modsListFilter.Text; enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) - .ForEach(c => c.Visible = !(c.UserData is ContentPackage p) + .ForEach(c => c.Visible = c.UserData is not ContentPackage p || ModNameMatches(p, str) && ModMatchesTickboxes(p, c)); } @@ -504,12 +513,12 @@ namespace Barotrauma.Steam //are enabled, and all files match either of them so show this mod } else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected - && p.Files.Any(f => !(f is BaseSubFile))) + && p.Files.Any(f => f is not BaseSubFile)) { matches = false; } else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected - && p.Files.Any(f => !(f is ItemAssemblyFile))) + && p.Files.Any(f => f is not ItemAssemblyFile)) { matches = false; } @@ -520,7 +529,7 @@ namespace Barotrauma.Steam private void PrepareToShowModInfo(ContentPackage mod) { if (!mod.UgcId.TryUnwrap(out var ugcId) - || !(ugcId is SteamWorkshopId workshopId)) { return; } + || ugcId is not SteamWorkshopId workshopId) { return; } TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value), t => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index c9cbd489d..3193aa9ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -44,12 +44,26 @@ namespace Barotrauma.Steam public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout - = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); + = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false) + { + Stretch = true, + AbsoluteSpacing = GUI.IntScale(4) + }; tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; tabContents = new Dictionary(); + new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft), + style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) + { + OnClicked = (button, o) => + { + SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); + return false; + } + }; + contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform), @@ -130,17 +144,8 @@ namespace Barotrauma.Steam { tabContents[Tab.PopularMods].Button.Enabled = false; } - GUIFrame listFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), content.RectTransform), style: null); + GUIFrame listFrame = new GUIFrame(new RectTransform(Vector2.One, content.RectTransform), style: null); CreateWorkshopItemList(listFrame, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); - new GUIButton(new RectTransform((1.0f, 0.05f), content.RectTransform, Anchor.BottomLeft), - style: "GUIButtonSmall", text: TextManager.Get("FindModsButton")) - { - OnClicked = (button, o) => - { - SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/"); - return false; - } - }; } private void CreatePublishTab(out GUIListBox selfModsList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 2d4a1ffb2..67bc81568 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -104,7 +104,7 @@ namespace Barotrauma } } - List successEffects = statusEffects.Where(se => se.type == ActionType.OnUse).ToList(); + List successEffects = statusEffects.Where(se => se.type == ActionType.OnSuccess).ToList(); List failureEffects = statusEffects.Where(se => se.type == ActionType.OnFailure).ToList(); foreach (StatusEffect statusEffect in successEffects) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 70941d22d..fdbf0053b 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.14.0 + 100.4.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -100,6 +100,8 @@ PreserveNewest + + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index f02309b56..79887d583 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.14.0 + 100.4.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -99,6 +99,7 @@ PreserveNewest + Icon.bmp diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 7f0257b84..a6b694a3d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.14.0 + 100.4.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma @@ -117,6 +117,7 @@ + @@ -126,6 +127,7 @@ + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index efd06e13b..1b587ef9d 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 100.4.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 2c9ca2ecf..d64e16e1e 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 100.4.0.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 49893f4dd..61369bc00 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -64,13 +64,18 @@ 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); msg.WriteByte((byte)Job.Variant); foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier)) { - msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier).Level); + msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f); } } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a3e74f054..eedb1f10b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -219,9 +219,9 @@ namespace Barotrauma else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) { #if DEBUG || UNSTABLE - DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); + DebugConsole.AddWarning($"Large discrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); #endif - LastNetworkUpdateID = networkUpdateID; + LastNetworkUpdateID = LastProcessedID = networkUpdateID; } if (memInput.Count > 60) { @@ -269,10 +269,13 @@ namespace Barotrauma case EventType.UpdateTalents: if (c.Character != this) { + if (!IsBot || !c.HasPermission(ClientPermissions.ManageBotTalents)) + { #if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - return; + return; + } } // get the full list of talents from the player, only give the ones @@ -549,7 +552,7 @@ namespace Barotrauma msg.WriteByte((byte)statType); foreach (var savedStatValue in Info.SavedStatValues[statType]) { - msg.WriteString(savedStatValue.StatIdentifier); + msg.WriteIdentifier(savedStatValue.StatIdentifier); msg.WriteSingle(savedStatValue.StatValue); msg.WriteBoolean(savedStatValue.RemoveOnDeath); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 29f57d01b..8a669f03e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1408,6 +1408,21 @@ namespace Barotrauma GameMain.Server.PrintSenderTransters(); })); + + AssignOnExecute("resetcharacternetstate", (string[] args) => + { + if (GameMain.Server == null) { return; } + + if (args.Length < 1) + { + ThrowError("Invalid parameters. The command should be formatted as \"resetcharacternetstate [character]\". If the names consist of multiple words, you should surround them with quotation marks."); + return; + } + + var character = FindMatchingCharacter(args.Skip(1).ToArray(), false); + character?.ResetNetState(); + }); + commands.Add(new Command("eventdata", "", (string[] args) => { if (args.Length == 0) { return; } @@ -1665,13 +1680,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); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index dfa4bc4c0..5d9dde87c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -31,7 +31,7 @@ namespace Barotrauma if (convAction.SelectedOption > -1) { //someone else already chose an option for this conversation: interrupt for this client - convAction.ServerWrite(convAction.speaker, sender, interrupt: true); + convAction.ServerWrite(convAction.Speaker, sender, interrupt: true); } else { 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 1cf319471..05e6c6f4e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -158,7 +158,7 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); if (doc?.Root == null) { - DebugConsole.ThrowError("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings."); + DebugConsole.AddWarning("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings."); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 52ef55b2f..4283c7206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -23,10 +23,8 @@ namespace Barotrauma return client.HasPermission(permissions) || client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Server.ConnectedClients.Count == 1 || IsOwner(client) || - //allow managing if no-one with permissions is alive - GameMain.Server.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + AnyOneAllowedToManageCampaign(permissions); } public bool AllowedToManageWallets(Client client) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 12d7a7af3..017d68101 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -335,7 +335,7 @@ namespace Barotrauma break; } - Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); + Map.ProgressWorld(this, transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); if (success) @@ -347,6 +347,8 @@ namespace Barotrauma (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c); } } + // Event history must be registered before ending the round or it will be cleared + GameMain.GameSession.EventManager.RegisterEventHistory(); } GameMain.GameSession.EndRound("", traitorResults, transitionType); @@ -360,7 +362,6 @@ namespace Barotrauma LeaveUnconnectedSubs(leavingSub); NextLevel = newLevel; GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - GameMain.GameSession.EventManager.RegisterEventHistory(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -384,17 +385,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.StartGame(); + 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.StartGame(); + } yield return CoroutineStatus.Success; } @@ -417,7 +415,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) @@ -486,7 +484,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(); } @@ -502,6 +501,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); @@ -697,10 +704,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) @@ -1014,12 +1017,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); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index e30b1148f..e46134ca6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.Networking; namespace Barotrauma @@ -30,6 +32,9 @@ namespace Barotrauma switch (header) { + case NetworkHeader.ADD_EVERYTHING_TO_PENDING: + ProcessAddEverything(sender); + break; case NetworkHeader.REQUEST_AFFLICTIONS: ProcessRequestedAfflictions(inc, sender); break; @@ -57,7 +62,14 @@ namespace Barotrauma NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); InsertPendingCrewMember(newCrewMember); - ServerSend(newCrewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + ServerSend(new NetCollection(newCrewMember), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessAddEverything(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + AddEverythingToPending(); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); } private void ProcessNewRemoval(IReadMessage inc, Client client) @@ -73,12 +85,7 @@ namespace Barotrauma { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } - INetSerializableStruct writeCrewMember = new NetPendingCrew - { - CrewMembers = PendingHeals.ToArray() - }; - - ServerSend(writeCrewMember, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); + ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); } private void ProcessHealing(Client client) @@ -107,10 +114,10 @@ namespace Barotrauma CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); - NetAffliction[] pendingAfflictions = Array.Empty(); + ImmutableArray pendingAfflictions = ImmutableArray.Empty; int infoId = 0; - if (foundInfo is { Character: { CharacterHealth: { } health } }) + if (foundInfo is { Character.CharacterHealth: { } health }) { pendingAfflictions = GetAllAfflictions(health); infoId = foundInfo.GetIdentifierUsingOriginalName(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 60cc5c609..4cb7e4fd8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components msg.WriteBoolean(launch); if (launch) { - msg.WriteUInt16(User.ID); + msg.WriteUInt16(User?.ID ?? 0); msg.WriteSingle(launchPos.X); msg.WriteSingle(launchPos.Y); msg.WriteSingle(launchRot); 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 862478ce6..50eb92e20 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -106,6 +106,14 @@ namespace Barotrauma $"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})"); } break; + case SetItemStatEventData setItemStatEventData: + msg.WriteByte((byte)setItemStatEventData.Stats.Count); + foreach (var (key, value) in setItemStatEventData.Stats) + { + msg.WriteNetSerializableStruct(key); + msg.WriteSingle(value); + } + break; case UpgradeEventData upgradeEventData: var upgrade = upgradeEventData.Upgrade; var upgradeTargets = upgrade.TargetComponents; @@ -243,6 +251,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 15f4d365a..9febcddaf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -152,7 +152,9 @@ namespace Barotrauma.Networking public bool IsBanned(AccountId accountId, out string reason) { RemoveExpired(); - var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)); + var bannedPlayer = + bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)) ?? + bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out Address adr) && adr is SteamP2PAddress steamAdr && steamAdr.SteamId.Equals(accountId)); reason = bannedPlayer?.Reason ?? string.Empty; return bannedPlayer != null; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index fdea971a5..c6d6559cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -40,6 +40,8 @@ namespace Barotrauma.Networking public float ChatSpamTimer; public int ChatSpamCount; + public string RejectedName; + public int RoundsSincePlayedAsTraitor; public float KickAFKTimer; @@ -69,6 +71,9 @@ namespace Barotrauma.Networking public DateTime JoinTime; + public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30); + public DateTime LastNameChangeTime; + private CharacterInfo characterInfo; public CharacterInfo CharacterInfo { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index f3e37ce04..666cd3039 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2516,6 +2516,7 @@ namespace Barotrauma.Networking msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest); msg.WriteBoolean(IsUsingRespawnShuttle()); msg.WriteByte((byte)ServerSettings.LosMode); + msg.WriteByte((byte)ServerSettings.ShowEnemyHealthBars); msg.WriteBoolean(includesFinalize); msg.WritePadBits(); ServerSettings.WriteMonsterEnabled(msg); @@ -2713,9 +2714,24 @@ namespace Barotrauma.Networking CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; } + + var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime; + if (timeSinceNameChange < Client.NameChangeCoolDown) + { + //only send once per second at most to prevent using this for spamming + if (timeSinceNameChange.TotalSeconds > 1) + { + var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; + SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); + } + c.NameId = nameId; + c.RejectedName = newName; + return false; + } + if (!newJob.IsEmpty) { - if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) + if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob) { newJob = Identifier.Empty; } @@ -2731,26 +2747,25 @@ namespace Barotrauma.Networking public bool TryChangeClientName(Client c, string newName) { newName = Client.SanitizeName(newName); - //update client list even if the name cannot be changed to the one sent by the client, - //so the client will be informed what their actual name is - LastClientListUpdateID++; - - if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; } - - if (IsNameValid(c, newName)) + if (newName != c.Name && !string.IsNullOrEmpty(newName) && IsNameValid(c, newName)) { + c.LastNameChangeTime = DateTime.Now; string oldName = c.Name; c.Name = newName; + c.RejectedName = string.Empty; SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); + LastClientListUpdateID++; return true; } else { + //update client list even if the name cannot be changed to the one sent by the client, + //so the client will be informed what their actual name is + LastClientListUpdateID++; return false; } } - private bool IsNameValid(Client c, string newName) { newName = Client.SanitizeName(newName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index abcbbf42c..f49b85fd2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -168,9 +168,10 @@ namespace Barotrauma.Networking if (!bufferedEvent.Character.IsIncapacitated && NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) { + DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}"); continue; } - + try { ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 4e16b15bb..302b5b959 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -250,7 +250,9 @@ namespace Barotrauma.Networking structToSend = new ServerPeerContentPackageOrderPacket { ServerName = GameMain.Server.ServerName, - ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) + ContentPackages = ContentPackageManager.EnabledPackages.All + .Where(cp => cp.Files.Any()) + .Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .ToImmutableArray() }; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 603683348..7e348523a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -272,7 +272,6 @@ namespace Barotrauma.Networking XDocument doc = new XDocument(new XElement("serversettings")); doc.Root.SetAttributeValue("name", ServerName); - doc.Root.SetAttributeValue("public", IsPublic); doc.Root.SetAttributeValue("port", Port); #if USE_STEAM doc.Root.SetAttributeValue("queryport", QueryPort); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 55a7ace33..38165fe37 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.14.0 + 100.4.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml index d88b556aa..40605ddf4 100644 --- a/Barotrauma/BarotraumaShared/Data/campaignsettings.xml +++ b/Barotrauma/BarotraumaShared/Data/campaignsettings.xml @@ -7,18 +7,21 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index f0eae6be8..cdeff1678 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 1708c86da..8279d6606 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -239,11 +239,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; @@ -303,7 +298,11 @@ namespace Barotrauma break; } } - + //pets are friendly! + if (PetBehavior != null || Character.Params.Group == "human") + { + Character.TeamID = CharacterTeamType.FriendlyNPC; + } ReevaluateAttacks(); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); @@ -442,6 +441,8 @@ 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)); if (steeringManager == insideSteering) @@ -547,8 +548,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)) @@ -573,6 +575,7 @@ namespace Barotrauma } else { + // Normally the monsters only use pathing inside submarines, not outside. if (Character.Submarine != null && Character.Params.UsePathFinding) { if (steeringManager != insideSteering) @@ -804,10 +807,6 @@ namespace Barotrauma Reverse = true; run = true; } - else - { - Reverse = false; - } SteeringManager.SteeringManual(deltaTime, dir * 0.2f); } else @@ -841,7 +840,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); } @@ -1137,7 +1136,6 @@ namespace Barotrauma return; } } - attackLimbSelectionTimer -= deltaTime; if (AttackLimb == null || attackLimbSelectionTimer <= 0) { @@ -1147,7 +1145,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)) @@ -1372,7 +1371,6 @@ namespace Barotrauma float distance = 0; Limb attackTargetLimb = null; - Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { if (!Character.AnimController.SimplePhysicsEnabled) @@ -1393,29 +1391,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; } } @@ -1423,7 +1421,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; @@ -1432,7 +1430,7 @@ namespace Barotrauma } // Check that we can reach the target - distance = toTarget.Length(); + distance = toTargetOffset.Length(); canAttack = distance < AttackLimb.attack.Range; if (canAttack) { @@ -1490,60 +1488,44 @@ namespace Barotrauma canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); if (canAttack && AttackLimb.attack.AvoidFriendlyFire) { - float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); - bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; - if (SwarmBehavior != null) + canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity)); + bool IsBlocked(Vector2 targetPosition) { - canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c)); - } - else - { - canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c)); - } - if (canAttack) - { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); - - bool IsBlocked(Vector2 targetPosition) + foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { - foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + Character hitTarget = null; + if (body.UserData is Character c) { - Character hitTarget = null; - if (body.UserData is Character c) - { - hitTarget = c; - } - else if (body.UserData is Limb limb) - { - hitTarget = limb.character; - } - if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) - { - return true; - } + hitTarget = c; + } + else if (body.UserData is Limb limb) + { + hitTarget = limb.character; + } + if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget)) + { + return true; } - return false; } + return false; } } } } } 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); @@ -1610,7 +1592,7 @@ namespace Barotrauma } } } - else + else if (!IsTryingToSteerThroughGap) { if (AttackLimb.attack.Ranged) { @@ -1631,6 +1613,10 @@ namespace Barotrauma SteeringManager.Reset(); } } + else + { + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition)); + } } else { @@ -1669,40 +1655,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; } @@ -1712,52 +1718,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; @@ -1769,7 +1799,6 @@ namespace Barotrauma break; } steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); if (IsBlocked(deltaTime, steerPos)) { if (!inverseDir) @@ -1781,7 +1810,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) { @@ -1791,16 +1820,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; @@ -1822,18 +1859,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; @@ -1850,35 +1888,79 @@ 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; + } - 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); + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.SwimFastParams.MovementSpeed * (targetSub != null ? 0.3f : 0.5f)); } } - else if (AttackLimb.attack.Ranged) + if (updateSteering) { - // Too close - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); - } - if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); + if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged) + { + bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f; + bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f); + if (fallBack) + { + Reverse = true; + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + else if (advance) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else if (!Character.InWater) + { + SteeringManager.Reset(); + FaceTarget(SelectedAiTarget.Entity); + } + } + else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) + { + if (pathSteering != null) + { + 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) || + AttackLimb != null && AttackLimb.attack.Ranged)) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); + } } } } + Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity; + if (AttackLimb?.attack is Attack { Ranged: true } attack && targetEntity != null) + { + AimRangedAttack(attack, targetEntity); + } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } @@ -1887,6 +1969,31 @@ namespace Barotrauma { AttackLimb.attack.ResetAttackTimer(); } + + void DisableAttacksIfLimbNotRanged() + { + if (AttackLimb?.attack is { Ranged: false }) + { + canAttack = false; + } + } + } + + public void AimRangedAttack(Attack attack, Entity targetEntity) + { + if (attack == null || attack.Ranged == false || targetEntity == null) { return; } + Character.SetInput(InputType.Aim, false, true); + if (attack.AimRotationTorque <= 0) { return; } + Limb limb = GetLimbToRotate(attack); + if (limb != null) + { + Vector2 toTarget = targetEntity.WorldPosition - limb.WorldPosition; + float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + limb.body.SuppressSmoothRotationCalls = false; + float angle = MathUtils.VectorToAngle(toTarget); + limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque); + limb.body.SuppressSmoothRotationCalls = true; + } } private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) @@ -1959,9 +2066,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; } } @@ -2114,13 +2230,15 @@ namespace Barotrauma } // 10 dmg, 100 health -> 0.1 - private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); + private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); - private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) + private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } - if (attackingLimb?.attack == null) { return false; } - ActiveAttack = attackingLimb.attack; + if (AttackLimb?.attack == 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. @@ -2131,92 +2249,106 @@ namespace Barotrauma State = AIState.Attack; } } - IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; - if (damageTarget != null) + if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) { - if (Character.Params.CanInteract && Character.Inventory != null) + Limb referenceLimb = GetLimbToRotate(ActiveAttack); + if (referenceLimb != null) { - // Use equipped items (weapons) - Item item = GetEquippedItem(attackingLimb); - if (item != null) + 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)); + if (angle > ActiveAttack.RequiredAngleToShoot) { - if (item.RequireAimToUse) + return true; + } + } + } + if (Character.Params.CanInteract && Character.Inventory != null) + { + // Use equipped items (weapons) + Item item = GetEquippedItem(AttackLimb); + if (item != null) + { + if (item.RequireAimToUse) + { + if (!Aim(deltaTime, spatialTarget, item)) { - if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) - { - // Valid target, but can't shoot -> return true so that it will not be ignored. - return true; - } + // Valid target, but can't shoot -> return true so that it will not be ignored. + return true; } + } + if (damageTarget != null) + { Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); item.Use(deltaTime, Character); } } - //simulate attack input to get the character to attack client-side - Character.SetInput(InputType.Attack, true, true); - if (!ActiveAttack.IsRunning) - { + } + if (damageTarget == null) { return true; } + //simulate attack input to get the character to attack client-side + Character.SetInput(InputType.Attack, true, true); + if (!ActiveAttack.IsRunning) + { #if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( - attackingLimb, - damageTarget, - targetLimb, - SimPosition)); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( + AttackLimb, + damageTarget, + targetLimb, + SimPosition)); #else - Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); + Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif - } - - if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) + } + if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) + { + if (ActiveAttack.CoolDownTimer > 0) { - if (attackingLimb.attack.CoolDownTimer > 0) + SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f)); + } + if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) + { + LatchOntoAI.SetAttachTarget(targetCharacter); + } + if (!ActiveAttack.Ranged) + { + if (damageTarget.Health > 0 && attackResult.Damage > 0) { - SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; - if (!(damageTarget is Character)) + 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) + else { - LatchOntoAI.SetAttachTarget(targetCharacter); - } - if (!attackingLimb.attack.Ranged) - { - if (damageTarget.Health > 0 && attackResult.Damage > 0) - { - // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon - float greed = AIParams.AggressionGreed; - if (!(damageTarget is Character)) - { - // Halve the greed for attacking non-characters. - greed /= 2; - } - selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; - } - else - { - selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); - return selectedTargetMemory.Priority > 1; - } + selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); + return selectedTargetMemory.Priority > 1; } } - return true; } - return false; + return true; } 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; @@ -2238,11 +2370,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); @@ -2299,7 +2431,6 @@ namespace Barotrauma { if (attackVector == null) { - // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); @@ -2319,6 +2450,16 @@ namespace Barotrauma return true; } + private Limb GetLimbToRotate(Attack attack) + { + Limb limb = AttackLimb; + if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length) + { + limb = Character.AnimController.Limbs[attack.RotationLimbIndex]; + } + return limb; + } + #endregion #region Eat @@ -2526,13 +2667,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) { @@ -2633,7 +2780,7 @@ namespace Barotrauma } else if (CanPassThroughHole(s, i)) { - valueModifier *= isInnerWall ? 1 : 0; + valueModifier *= isInnerWall ? 0.5f : 0; } else if (!canAttackWalls) { @@ -3429,7 +3576,7 @@ namespace Barotrauma private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); - private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false) + private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false) { if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) { @@ -3437,6 +3584,11 @@ namespace Barotrauma { if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams)) { + if (state == AIState.Attack) + { + // Only applies to new temp target params. Shouldn't affect any existing definitions (handled below). + targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub; + } tempParams.Add(tag, targetParams); } } @@ -3470,7 +3622,7 @@ namespace Barotrauma { isStateChanged = true; SetStateResetTimer(); - ChangeParams(target.SpeciesName, state, priority); + ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman); if (target.IsHuman) { priority = GetTargetParams("human")?.Priority; @@ -3518,6 +3670,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) @@ -3539,6 +3696,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); @@ -3554,7 +3716,10 @@ namespace Barotrauma { // We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible. // When the monsters attack the player sub, they wall hack so that they can be more aggressive. - checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; + // Pets should always check the visibility, unless the pet and the target are both outside the submarine -> shouldn't target when they can't perceive (= no wall hack) + checkVisibility = + Character.IsPet && (Character.Submarine == null) != (target.Entity.Submarine == null) || + target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None; } if (dist > 0) { @@ -3639,6 +3804,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) @@ -3698,6 +3864,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 3ed35dd0a..1cec65d36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -395,6 +395,10 @@ namespace Barotrauma } objectiveManager.UpdateObjectives(deltaTime); + if (reportProblemsTimer > 0) + { + reportProblemsTimer -= deltaTime; + } if (reactTimer > 0.0f) { reactTimer -= deltaTime; @@ -407,7 +411,6 @@ namespace Barotrauma else { Character.UpdateTeam(); - if (Character.CurrentHull != null) { if (Character.IsOnPlayerTeam) @@ -425,19 +428,15 @@ namespace Barotrauma } } } - if (Character.SpeechImpediment < 100.0f) + if (reportProblemsTimer <= 0.0f) { - reportProblemsTimer -= deltaTime; - if (reportProblemsTimer <= 0.0f) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { - if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) - { - ReportProblems(); - } - reportProblemsTimer = reportProblemsInterval; + ReportProblems(); } - UpdateSpeaking(); + reportProblemsTimer = reportProblemsInterval; } + UpdateSpeaking(); UnequipUnnecessaryItems(); reactTimer = GetReactionTime(); } @@ -912,7 +911,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; - bool speak = true; + bool speak = Character.SpeechImpediment < 100; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -1063,17 +1062,15 @@ namespace Barotrauma private void UpdateSpeaking() { if (!Character.IsOnPlayerTeam) { return; } - + if (Character.SpeechImpediment >= 100) { return; } if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); } - if (Character.Bleeding > 2.0f) { Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) { Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); @@ -1517,7 +1514,7 @@ namespace Barotrauma startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f); //do a raycast upwards to find any walls - float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f; + float minCeilingDist = Character.AnimController.Collider.Height / 2 + Character.AnimController.Collider.Radius + 0.1f; shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null; } @@ -1615,7 +1612,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; @@ -1628,7 +1625,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) { var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); @@ -1724,7 +1721,7 @@ namespace Barotrauma 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); @@ -1860,6 +1857,7 @@ namespace Barotrauma bool targetAdded = false; DoForEachCrewMember(caller, humanAI => { + if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } var objective = humanAI.ObjectiveManager.GetObjective(); if (objective != null) { @@ -2021,7 +2019,7 @@ namespace Barotrauma bool friendlyTeam = IsOnFriendlyTeam(me, other); bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; if (!teamGood) { return false; } - bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); + bool speciesGood = other.IsPet || other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); if (!speciesGood) { return false; } if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 5dc043af1..9c3b8c40b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -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; } @@ -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(); 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 f3a2a9c39..cf6e08e81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -88,6 +88,10 @@ namespace Barotrauma { currentFlags.Add("EnterOutpost".ToIdentifier()); } + if (Level.Loaded.IsEndBiome) + { + currentFlags.Add("EndLevel".ToIdentifier()); + } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { @@ -126,6 +130,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 c0c6f9c69..0a509fdd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -748,6 +748,9 @@ namespace Barotrauma } if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType)) { + //clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter + character.ClearInput(InputType.Aim); + character.ClearInput(InputType.Shoot); Weapon.TryInteract(character, forceSelectKey: true); var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s)); if (character.Inventory.TryPutItem(Weapon, character, slots)) @@ -764,7 +767,7 @@ namespace Barotrauma } return true; - bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); + static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand); } private float findHullTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 0704b91c6..cd48340a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -23,8 +23,8 @@ namespace Barotrauma protected override float TargetEvaluation() { - if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; } - int totalEnemies = Targets.Count(); + if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return Targets.None() ? 0 : 100; } + int totalEnemies = Targets.Count; if (totalEnemies == 0) { return 0; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } @@ -67,7 +67,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/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index d8607e729..87de7bba5 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index aed77cb77..5a8c8bbd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -222,7 +222,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(); } @@ -290,7 +290,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/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 30d687a2a..7af3858b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -502,10 +502,12 @@ namespace Barotrauma public static IEnumerable GetTreatableAfflictions(Character character) { - foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions()) + var allAfflictions = character.CharacterHealth.GetAllAfflictions(); + foreach (Affliction affliction in allAfflictions) { if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; } + if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; } yield return affliction; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 1105eab19..24f0ef67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using static Barotrauma.CharacterParams; namespace Barotrauma { @@ -44,7 +45,7 @@ namespace Barotrauma public float PlayTimer { get; set; } private float? unstunY { get; set; } - public EnemyAIController AiController { get; private set; } = null; + public EnemyAIController AIController { get; private set; } = null; public Character Owner { get; set; } @@ -134,8 +135,8 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); + Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); break; } } @@ -160,8 +161,8 @@ namespace Barotrauma public PetBehavior(XElement element, EnemyAIController aiController) { - AiController = aiController; - AiController.Character.CanBeDragged = true; + AIController = aiController; + AIController.Character.CanBeDragged = true; MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f); MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f); @@ -218,7 +219,7 @@ namespace Barotrauma bool success = OnEat(item.GetTags()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier); } return success; } @@ -229,7 +230,7 @@ namespace Barotrauma bool success = OnEat("dead".ToIdentifier()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName); } return success; } @@ -252,7 +253,7 @@ namespace Barotrauma Hunger += foods[i].Hunger; Happiness += foods[i].Happiness; #if CLIENT - AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); + AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); #endif return true; } @@ -265,20 +266,20 @@ namespace Barotrauma if (PlayTimer > 0.0f) { return; } if (Owner == null) { Owner = player; } PlayTimer = 5.0f; - AiController.Character.IsRagdolled = true; + AIController.Character.IsRagdolled = true; Happiness += 10.0f; - AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); - unstunY = AiController.Character.SimPosition.Y; + AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); + unstunY = AIController.Character.SimPosition.Y; #if CLIENT - AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); + AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); #endif } public string GetTagName() { - if (AiController.Character.Inventory != null) + if (AIController.Character.Inventory != null) { - foreach (Item item in AiController.Character.Inventory.AllItems) + foreach (Item item in AIController.Character.Inventory.AllItems) { var tag = item.GetComponent(); if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) @@ -293,7 +294,7 @@ namespace Barotrauma public void Update(float deltaTime) { - var character = AiController.Character; + var character = AIController.Character; if (character?.Removed ?? true || character.IsDead) { return; } if (unstunY.HasValue) @@ -332,16 +333,27 @@ namespace Barotrauma Food food = foods[i]; if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y) { - if (food.TargetParams == null && - AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams)) + if (food.TargetParams == null) { - targetParams.IgnoreContained = food.IgnoreContained; - food.TargetParams = targetParams; + if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target)) + { + food.TargetParams = target; + } + else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams)) + { + food.TargetParams = targetParams; + } + if (food.TargetParams != null) + { + food.TargetParams.State = AIState.Eat; + food.TargetParams.Priority = food.Priority; + food.TargetParams.IgnoreContained = food.IgnoreContained; + } } } else if (food.TargetParams != null) { - AiController.AIParams.RemoveTarget(food.TargetParams); + AIController.AIParams.RemoveTarget(food.TargetParams); food.TargetParams = null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 95ff37b9c..b1a83f852 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -116,10 +116,10 @@ namespace Barotrauma } // accept only the highest priority order - if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) + if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) { #if DEBUG - ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); + ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended."); #endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index a112758f1..b4a18d756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -356,7 +356,7 @@ namespace Barotrauma ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } - foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) + foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint"))) { var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); 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 ceeeab73b..bfc1d97ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -8,16 +8,81 @@ 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; + } + } + 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 +90,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 +109,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 +120,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 +181,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 +189,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 +198,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 +218,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; @@ -228,7 +269,7 @@ namespace Barotrauma foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + if (Vector2.DistanceSquared(submarine.WorldPosition, Submarine.WorldPosition) < minDist * minDist) { someoneNearby = true; break; @@ -237,14 +278,14 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } - if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + if (Vector2.DistanceSquared(c.WorldPosition, Submarine.WorldPosition) < minDist * minDist) { someoneNearby = true; break; } } if (!someoneNearby) { return; } - OperateTurrets(deltaTime); + OperateTurrets(deltaTime, Config.Entity); if (!IsClient) { if (!initialCellsSpawned) { SpawnInitialCells(); } @@ -287,7 +328,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 +355,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 +374,7 @@ namespace Barotrauma public void Remove() { Kill(); - RemoveThalamusItems(Wreck); + RemoveThalamusItems(Submarine); thalamusItems?.Clear(); thalamusStructures?.Clear(); } @@ -385,7 +426,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; @@ -396,7 +437,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; } @@ -422,19 +463,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 928dd8e74..a2e4e7a3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -87,7 +87,7 @@ namespace Barotrauma } public bool CanWalk => RagdollParams.CanWalk; - public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); + public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false }; // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index f0818dd66..a54535f6f 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 e2d69fe08..3bdb4c978 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -168,7 +168,7 @@ namespace Barotrauma { get { - float shoulderHeight = Collider.height / 2.0f; + float shoulderHeight = Collider.Height / 2.0f; if (inWater) { shoulderHeight += 0.4f; @@ -299,7 +299,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(); @@ -610,15 +610,18 @@ namespace Barotrauma torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!head.Disabled) { - float headAngle = HeadAngle.Value; - if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } - head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); - } - else - { - RotateHead(head); + if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + { + float headAngle = HeadAngle.Value; + if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } + head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); + } + else + { + RotateHead(head); + } } if (!onGround) @@ -1117,7 +1120,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; @@ -1212,7 +1215,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) && @@ -1389,7 +1392,7 @@ namespace Barotrauma target.Oxygen += deltaTime * 0.5f; //Stabilize them } - bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR); + float cprBoost = character.GetStatValue(StatTypes.CPRBoost); int skill = (int)character.GetSkillLevel("medical"); //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) @@ -1406,7 +1409,7 @@ namespace Barotrauma { if (target.Oxygen < -10.0f) { - if (powerfulCPR) + if (cprBoost >= 1f) { //prevent the patient from suffocating no matter how fast their oxygen level is dropping target.Oxygen = Math.Max(target.Oxygen, -10.0f); @@ -1453,7 +1456,7 @@ namespace Barotrauma reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); - if (powerfulCPR) { reviveChance *= 2.0f; } + reviveChance *= 1f + cprBoost; if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { @@ -1833,8 +1836,6 @@ namespace Barotrauma { heldItem.FlipX(relativeToSub: false); } - // TODO: was this added by a mistake? - //heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index affc19c14..5aad72e9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -169,18 +169,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; @@ -571,6 +571,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; @@ -676,6 +680,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) @@ -873,7 +881,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered) { continue; } + if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; } limb.Dir = Dir; limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y); limb.MirrorPullJoint(); @@ -1279,7 +1287,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) @@ -1597,7 +1605,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) @@ -1787,7 +1795,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 d5d02d2d4..06b7c21cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -180,15 +181,30 @@ 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] + public float RequiredAngleToShoot { get; set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable] + public float AimRotationTorque { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable] + public int RotationLimbIndex { get; set; } + + [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. /// @@ -521,7 +537,7 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) @@ -529,6 +545,12 @@ namespace Barotrauma effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); } } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { + targets.Clear(); + targets.AddRange(attacker.Inventory.AllItems); + effect.Apply(effectType, deltaTime, attacker, targets); + } } return attackResult; @@ -554,7 +576,15 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration); + float penetration = Penetration; + + float? penetrationValue = SourceItem?.GetComponent()?.Penetration; + if (penetrationValue.HasValue) + { + penetration += penetrationValue.Value; + } + + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -584,13 +614,19 @@ namespace Barotrauma effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effectType, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); } + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) + { + targets.Clear(); + targets.AddRange(attacker.Inventory.AllItems); + effect.Apply(effectType, deltaTime, attacker, targets); + } } return attackResult; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 0da21fd42..d44450a24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -129,6 +129,13 @@ namespace Barotrauma public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active); public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public bool 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 @@ -178,6 +185,13 @@ namespace Barotrauma } } + + private CharacterTeamType? originalTeamID; + public CharacterTeamType OriginalTeamID + { + get { return originalTeamID ?? teamID; } + } + private Wallet wallet; public Wallet Wallet @@ -199,7 +213,7 @@ namespace Barotrauma protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; - const string OriginalTeamIdentifier = "original"; + private const string OriginalChangeTeamIdentifier = "original"; private void ThrowIfAccessingWalletsInSingleplayer() { @@ -214,20 +228,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; @@ -271,7 +281,7 @@ namespace Barotrauma { if (currentTeamChange == removedTeamChange) { - currentTeamChange = activeTeamChanges[OriginalTeamIdentifier]; + currentTeamChange = activeTeamChanges[OriginalChangeTeamIdentifier]; } } return activeTeamChanges.Remove(identifier); @@ -305,7 +315,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; @@ -611,7 +623,9 @@ namespace Barotrauma CharacterHealth.SetHealthBarVisibility(value == null); #endif bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true }; - if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) + CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value)); + + if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet }) { #if SERVER if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings }) @@ -999,7 +1013,7 @@ namespace Barotrauma } } - public bool InWater => AnimController?.InWater ?? false; + public bool InWater => AnimController is AnimController { InWater: true }; public bool GodMode = false; @@ -1053,6 +1067,8 @@ namespace Barotrauma } } + public HashSet MarkedAsLooted = new(); + public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID; public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath); @@ -1574,19 +1590,37 @@ namespace Barotrauma } if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()])); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item)); } } } public float GetSkillLevel(string skillIdentifier) => GetSkillLevel(skillIdentifier.ToIdentifier()); - + + private static readonly ImmutableDictionary overrideStatTypes = new Dictionary + { + { new("helm"), StatTypes.HelmSkillOverride }, + { new("medical"), StatTypes.MedicalSkillOverride }, + { new("weapons"), StatTypes.WeaponsSkillOverride }, + { new("electrical"), StatTypes.ElectricalSkillOverride }, + { new("mechanical"), StatTypes.MechanicalSkillOverride } + }.ToImmutableDictionary(); + public float GetSkillLevel(Identifier skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType)) + { + float skillOverride = GetStatValue(statType); + if (skillOverride > skillLevel) + { + skillLevel = skillOverride; + } + } + // apply multipliers first so that multipliers only affect base skill value foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) { @@ -1617,6 +1651,7 @@ namespace Barotrauma skillLevel += GetStatValue(GetSkillStatType(skillIdentifier)); + return skillLevel; } @@ -1948,6 +1983,15 @@ namespace Barotrauma } } #endif + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Controlled != this && IsKeyDown(InputType.Aim)) + { + if (currentAttackTarget.AttackLimb?.attack is Attack { Ranged: true } attack && AIController is EnemyAIController enemyAi) + { + enemyAi.AimRangedAttack(attack, currentAttackTarget.DamageTarget as Entity); + } + } + if (attackCoolDown > 0.0f) { attackCoolDown -= deltaTime; @@ -1958,7 +2002,7 @@ namespace Barotrauma { if ((currentAttackTarget.DamageTarget as Entity)?.Removed ?? false) { - currentAttackTarget = default(AttackTargetData); + currentAttackTarget = default; } currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); } @@ -2053,58 +2097,56 @@ namespace Barotrauma } } - bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; - if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) + if (Inventory != null) { - foreach (Item item in HeldItems) + bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected; + if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem)) { - if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) + foreach (Item item in HeldItems) { - item.SecondaryUse(deltaTime, this); + tryUseItem(item, deltaTime); } - if (IsKeyDown(InputType.Use) && !item.IsShootable) + foreach (Item item in Inventory.AllItems) { - if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) + if (item.GetComponent() is { AllowUseWhenWorn: true } && HasEquippedItem(item)) { - item.Use(deltaTime, this); + tryUseItem(item, deltaTime); } } - if (IsKeyDown(InputType.Shoot) && item.IsShootable) + } + } + + void tryUseItem(Item item, float deltaTime) + { + if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) + { + item.SecondaryUse(deltaTime, this); + } + if (IsKeyDown(InputType.Use) && !item.IsShootable) + { + if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) { - if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) - { - item.Use(deltaTime, this); - } + item.Use(deltaTime, this); + } + } + if (IsKeyDown(InputType.Shoot) && item.IsShootable) + { + if (!item.RequireAimToUse || IsKeyDown(InputType.Aim)) + { + item.Use(deltaTime, this); + } #if CLIENT - else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) - { - HintManager.OnShootWithoutAiming(this, item); - } -#endif + else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim)) + { + HintManager.OnShootWithoutAiming(this, item); } +#endif } } 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) @@ -2878,7 +2920,9 @@ namespace Barotrauma for (int i = 0; i < CharacterList.Count; i++) { - CharacterList[i].Update(deltaTime, cam); + var character = CharacterList[i]; + System.Diagnostics.Debug.Assert(character != null && !character.Removed); + character.Update(deltaTime, cam); } } @@ -3788,7 +3832,7 @@ namespace Barotrauma return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, bool ignoreSeveranceProbabilityModifier = false, Character attacker = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } #if DEBUG @@ -3798,7 +3842,7 @@ namespace Barotrauma return; } #endif - if (damage < targetLimb.Params.MinSeveranceDamage) { return; } + if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; } if (!IsDead) { if (!allowBeheading && targetLimb.type == LimbType.Head) { return; } @@ -3816,7 +3860,7 @@ namespace Barotrauma var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB; if (referenceLimb != targetLimb) { continue; } float probability = severLimbsProbability; - if (!IsDead) + if (!IsDead && !ignoreSeveranceProbabilityModifier) { probability *= joint.Params.SeveranceProbabilityModifier; } @@ -3938,13 +3982,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) { @@ -3992,7 +4029,12 @@ namespace Barotrauma 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; } @@ -4075,7 +4117,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, this, targets); } else if (statusEffect.targetLimbs != null) @@ -4709,6 +4751,8 @@ namespace Barotrauma public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; + 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); @@ -4789,6 +4833,32 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + private readonly HashSet sameRoomHulls = new(); + + /// + /// Check if the character is in the same room + /// Room and hull differ in that a room can consist of multiple linked hulls + /// + public bool IsInSameRoomAs(Character character) + { + if (character == this) { return true; } + + if (character.CurrentHull is null || CurrentHull is null) + { + // Outside doesn't count as a room + return false; + } + + if (character.Submarine != Submarine) { return false; } + if (character.CurrentHull == CurrentHull) { return true; } + + sameRoomHulls.Clear(); + CurrentHull.GetLinkedEntities(sameRoomHulls); + sameRoomHulls.Add(CurrentHull); + + return sameRoomHulls.Contains(character.CurrentHull); + } + public bool HasUnlockedAllTalents() { if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) @@ -4797,7 +4867,7 @@ namespace Barotrauma { foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) { - if (talentOption.TalentIdentifiers.None(t => HasTalent(t))) + if (talentOption.TalentIdentifiers.None(HasTalent)) { return false; } @@ -4842,6 +4912,19 @@ namespace Barotrauma return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); } + public bool HasStoreAccessForItem(ItemPrefab prefab) + { + foreach (CharacterTalent talent in characterTalents) + { + foreach (Identifier unlockedItem in talent.UnlockedStoreItems) + { + if (prefab.Tags.Contains(unlockedItem)) { return true; } + } + } + + return false; + } + /// /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. /// @@ -4900,7 +4983,7 @@ namespace Barotrauma /// private readonly Dictionary wearableStatValues = new Dictionary(); - public float GetStatValue(StatTypes statType) + public float GetStatValue(StatTypes statType, bool includeSaved = true) { if (!IsHuman) { return 0f; } @@ -4913,7 +4996,7 @@ namespace Barotrauma { statValue += CharacterHealth.GetStatValue(statType); } - if (Info != null) + if (Info != null && includeSaved) { // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change statValue += Info.GetSavedStatValue(statType); @@ -5054,6 +5137,16 @@ namespace Barotrauma } } + internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter + { + public Character Character { get; set; } + + public AbilityCharacterLoot(Character character) + { + Character = character; + } + } + class AbilityCharacterKill : AbilityObject, IAbilityCharacter { public AbilityCharacterKill(Character character, Character killer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 6876a6a22..a2768f980 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -305,6 +305,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 /// @@ -657,7 +659,6 @@ namespace Barotrauma { Name = GetRandomName(randSync); } - TryLoadNameAndTitle(npcIdentifier); SetPersonalityTrait(); @@ -735,9 +736,7 @@ namespace Barotrauma Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); Salary = infoElement.GetAttributeInt("salary", 1000); - ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); - UnlockedTalents = new HashSet(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty())); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); LoadTagsBackwardsCompatibility(infoElement, tags); @@ -813,18 +812,24 @@ namespace Barotrauma infoElement.GetAttributeIdentifier("npcid", Identifier.Empty)); 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; - if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) + + Identifier elementName = subElement.Name.ToIdentifier(); + + if (elementName == "job" && !jobCreated) { Job = new Job(subElement); jobCreated = true; // there used to be a break here, but it had to be removed to make room for statvalues // using the jobCreated boolean to make sure that only the first job found is created } - else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase)) + else if (elementName == "savedstatvalues") { foreach (XElement savedStat in subElement.Elements()) { @@ -838,8 +843,8 @@ namespace Barotrauma float value = savedStat.GetAttributeFloat("statvalue", 0f); if (value == 0f) { continue; } - string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant(); - if (string.IsNullOrEmpty(statIdentifier)) + Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) { DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!"); return; @@ -849,6 +854,20 @@ namespace Barotrauma ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); } } + else if (elementName == "talents") + { + Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe + + foreach (XElement talentElement in subElement.Elements()) + { + if (talentElement.Name.ToIdentifier() != "talent") { continue; } + + Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (talentIdentifier == Identifier.Empty) { continue; } + + UnlockedTalents.Add(talentIdentifier); + } + } } LoadHeadAttachments(); } @@ -1125,7 +1144,7 @@ namespace Barotrauma partial void LoadAttachmentSprites(); - private int CalculateSalary() + public int CalculateSalary() { if (Name == null || Job == null) { return 0; } @@ -1149,13 +1168,17 @@ namespace Barotrauma increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed); + increase = GetSkillSpecificGain(increase, skillIdentifier); + float prevLevel = Job.GetSkillLevel(skillIdentifier); Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); float newLevel = Job.GetSkillLevel(skillIdentifier); if ((int)newLevel > (int)prevLevel) - { + { + float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain); + Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); // assume we are getting at least 1 point in skill, since this logic only runs in such cases float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f); var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility); @@ -1169,6 +1192,25 @@ namespace Barotrauma OnSkillChanged(skillIdentifier, prevLevel, newLevel); } + private static readonly ImmutableDictionary skillGainStatValues = new Dictionary + { + { new("helm"), StatTypes.HelmSkillGainSpeed }, + { new("medical"), StatTypes.WeaponsSkillGainSpeed }, + { new("weapons"), StatTypes.MedicalSkillGainSpeed }, + { new("electrical"), StatTypes.ElectricalSkillGainSpeed }, + { new("mechanical"), StatTypes.MechanicalSkillGainSpeed } + }.ToImmutableDictionary(); + + private float GetSkillSpecificGain(float increase, Identifier skillIdentifier) + { + if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType)) + { + increase *= 1f + Character.GetStatValue(statType); + } + + return increase; + } + public void SetSkillLevel(Identifier skillIdentifier, float level) { if (Job == null) { return; } @@ -1194,10 +1236,6 @@ namespace Barotrauma int prevAmount = ExperiencePoints; var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); - if (isMissionExperience) - { - Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); - } experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0; amount = (int)(amount * experienceGainMultiplier.Value); @@ -1314,7 +1352,6 @@ namespace Barotrauma new XAttribute("tags", string.Join(",", Head.Preset.TagSet)), new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), - new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("additionaltalentpoints", AdditionalTalentPoints), new XAttribute("hairindex", Head.HairIndex), new XAttribute("beardindex", Head.BeardIndex), @@ -1337,6 +1374,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) @@ -1363,7 +1407,16 @@ namespace Barotrauma } } + XElement talentElement = new XElement("Talents"); + talentElement.Add(new XAttribute("version", GameMain.Version.ToString())); + + foreach (Identifier talentIdentifier in UnlockedTalents) + { + talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier))); + } + charElement.Add(savedStatElement); + charElement.Add(talentElement); parentElement?.Add(charElement); return charElement; } @@ -1717,20 +1770,33 @@ namespace Barotrauma } } - public void ResetSavedStatValue(string statIdentifier) + public void ResetSavedStatValue(Identifier statIdentifier) { foreach (StatTypes statType in SavedStatValues.Keys) { bool changed = false; foreach (SavedStatValue savedStatValue in SavedStatValues[statType]) { - if (savedStatValue.StatIdentifier != statIdentifier) { continue; } + if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; } + if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; } savedStatValue.StatValue = 0.0f; changed = true; } if (changed) { OnPermanentStatChanged(statType); } } + + static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier) + { + if (statIdentifier == identifier) { return true; } + + if (identifier.IndexOf('*') is var index and > -1) + { + return statIdentifier.StartsWith(identifier[0..index]); + } + + return false; + } } public float GetSavedStatValue(StatTypes statType) @@ -1748,7 +1814,7 @@ namespace Barotrauma { if (SavedStatValues.TryGetValue(statType, out var statValues)) { - return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue); + return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue); } else { @@ -1756,7 +1822,7 @@ namespace Barotrauma } } - public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) + public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false) { if (!SavedStatValues.ContainsKey(statType)) { @@ -1779,13 +1845,13 @@ namespace Barotrauma } } - public class SavedStatValue + internal sealed class SavedStatValue { - public string StatIdentifier { get; set; } + public Identifier StatIdentifier { get; set; } public float StatValue { get; set; } public bool RemoveOnDeath { get; set; } - public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) + public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath) { StatValue = value; RemoveOnDeath = removeOnDeath; @@ -1793,7 +1859,7 @@ namespace Barotrauma } } - class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter + internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 4aebc88c4..e51a8acd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -71,6 +71,13 @@ namespace Barotrauma /// public Character Source; + private readonly static LocalizedString[] strengthTexts = new LocalizedString[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; + public Affliction(AfflictionPrefab prefab, float strength) { #if CLIENT @@ -89,6 +96,7 @@ namespace Barotrauma } } + public void Serialize(XElement element) { SerializableProperty.SerializeProperties(this, element); @@ -108,6 +116,17 @@ namespace Barotrauma public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; + public LocalizedString GetStrengthText() + { + return GetStrengthText(Strength, Prefab.MaxStrength); + } + + public static LocalizedString GetStrengthText(float strength, float maxStrength) + { + return strengthTexts[ + MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + } + public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); public float GetVitalityDecrease(CharacterHealth characterHealth) @@ -429,7 +448,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets)); + statusEffect.AddNearbyTargets(characterHealth.Character.WorldPosition, targets); statusEffect.Apply(type, deltaTime, characterHealth.Character, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index f6da0bfd1..44643c736 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -10,7 +10,8 @@ public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { base.Update(characterHealth, targetLimb, deltaTime); - characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime; + float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier); + characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime; if (Source != null) { characterHealth.BloodlossAffliction.Source = Source; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index cd6e25f64..98274a891 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(); @@ -216,7 +216,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 a18f012bc..1d41a0153 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -67,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); @@ -109,7 +109,6 @@ namespace Barotrauma public float TransformThresholdOnDeath; public readonly Identifier HuskedSpeciesName; - public readonly Identifier[] TargetSpecies; public readonly bool TransferBuffs; public readonly bool SendMessages; @@ -214,7 +213,6 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.No)] public Identifier DialogFlag { get; private set; } - [Serialize("", IsPropertySaveable.No)] public Identifier Tag { get; private set; } @@ -276,6 +274,47 @@ namespace Barotrauma } } + public class Description + { + public enum TargetType + { + Any, + Self, + OtherCharacter + } + + public readonly LocalizedString Text; + public readonly Identifier TextTag; + public readonly float MinStrength, MaxStrength; + public readonly TargetType Target; + + public Description(ContentXElement element, AfflictionPrefab affliction) + { + TextTag = element.GetAttributeIdentifier("textidentifier", Identifier.Empty); + if (!TextTag.IsEmpty) + { + Text = TextManager.Get(TextTag); + } + string text = element.GetAttributeString("text", string.Empty); + if (!text.IsNullOrEmpty()) + { + Text = Text?.Fallback(text) ?? text; + } + else if (TextTag.IsEmpty) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions."); + } + + MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + if (MinStrength >= MaxStrength) + { + DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min."); + } + Target = element.GetAttributeEnum(nameof(Target), TargetType.Any); + } + } + public class PeriodicEffect { public readonly List StatusEffects = new List(); @@ -313,7 +352,6 @@ namespace Barotrauma public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; public override void Dispose() { } public static IEnumerable List => Prefabs; @@ -330,15 +368,21 @@ namespace Barotrauma //(e.g. mental health problems on head, lack of oxygen on torso...) public readonly LimbType IndicatorLimb; - public readonly LocalizedString Name, Description; + public readonly LocalizedString Name; public readonly Identifier TranslationIdentifier; public readonly bool IsBuff; + public readonly bool AffectMachines; public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; public readonly int BaseHealCost; public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; + private readonly LocalizedString defaultDescription; + public readonly ImmutableList Descriptions; + + public readonly bool HideIconAfterDelay; + //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI @@ -355,6 +399,11 @@ namespace Barotrauma //how strong the affliction needs to be before bots attempt to treat it public readonly float TreatmentThreshold = 5.0f; + /// + /// Bots will not try to treat the affliction if the character has any of these afflictions + /// + public ImmutableHashSet IgnoreTreatmentIfAfflictedBy; + /// /// The affliction is automatically removed after this time. 0 = unlimited /// @@ -384,6 +433,10 @@ namespace Barotrauma private readonly ConstructorInfo constructor; + public Identifier[] TargetSpecies { get; protected set; } + + public readonly bool ResetBetweenRounds; + public IEnumerable> TreatmentSuitability { get @@ -411,13 +464,14 @@ namespace Barotrauma { Name = Name.Fallback(fallbackName); } - Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); + defaultDescription = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}"); string fallbackDescription = element.GetAttributeString("description", ""); if (!string.IsNullOrEmpty(fallbackDescription)) { - Description = Description.Fallback(fallbackDescription); + defaultDescription = defaultDescription.Fallback(fallbackDescription); } - IsBuff = element.GetAttributeBool("isbuff", false); + IsBuff = element.GetAttributeBool(nameof(IsBuff), false); + AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", !IsBuff && @@ -426,6 +480,8 @@ namespace Barotrauma HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0); + IgnoreTreatmentIfAfflictedBy = element.GetAttributeIdentifierArray(nameof(IgnoreTreatmentIfAfflictedBy), Array.Empty()).ToImmutableHashSet(); + Duration = element.GetAttributeFloat(nameof(Duration), 0.0f); if (element.GetAttribute("nameidentifier") != null) @@ -443,28 +499,35 @@ namespace Barotrauma } } - ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); - ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); - ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold); - MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); - GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); + HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", + ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f); + ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f)); + ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold); + MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f); + GrainBurst = element.GetAttributeFloat(nameof(GrainBurst), 0.0f); + + ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold), Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); - TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); + TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f)); - DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); - BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f); + DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f); + BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f); - KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); + 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", "")); - IconColors = element.GetAttributeColorArray("iconcolors", null); - AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); - AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); + IconColors = element.GetAttributeColorArray(nameof(IconColors), null); + AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false); + AchievementOnRemoved = element.GetAttributeIdentifier(nameof(AchievementOnRemoved), ""); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); + + ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false); + + List descriptions = new List(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -481,15 +544,38 @@ namespace Barotrauma case "effect": case "periodiceffect": break; + case "description": + descriptions.Add(new Description(subElement, this)); + break; default: DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); break; } } + Descriptions = descriptions.ToImmutableList(); constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } + public LocalizedString GetDescription(float strength, Description.TargetType targetType) + { + foreach (var description in Descriptions) + { + if (strength < description.MinStrength || strength > description.MaxStrength) { continue; } + switch (targetType) + { + case Description.TargetType.Self: + if (description.Target == Description.TargetType.OtherCharacter) { continue; } + break; + case Description.TargetType.OtherCharacter: + if (description.Target == Description.TargetType.Self) { continue; } + break; + } + return description.Text; + } + return defaultDescription; + } + public static void LoadAllEffects() { Prefabs.ForEach(p => p.LoadEffects()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index cd028c9ca..f9f13e1b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -104,7 +104,7 @@ namespace Barotrauma public bool DoesBleed { - get => Character.Params.Health.DoesBleed; + get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine; private set => Character.Params.Health.DoesBleed = value; } @@ -140,9 +140,20 @@ namespace Barotrauma private float vitality; public float Vitality { - get - { - return Character.IsDead ? minVitality : vitality; + get + { + if (Character.IsDead) + { + return minVitality; + } + + if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions)) + { + return Math.Max(vitality, MinVitality + 1); + } + + return vitality; + } private set { @@ -539,7 +550,7 @@ namespace Barotrauma amount -= reduceAmount; if (treatmentAction != null) { - if (treatmentAction.Value == ActionType.OnUse) + if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess) { matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } @@ -679,17 +690,12 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true) { + 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") { return; } if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } - if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) - { - if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) - { - return; - } - } + if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } Affliction existingAffliction = null; foreach (KeyValuePair kvp in afflictions) @@ -868,19 +874,24 @@ namespace Barotrauma { if (!Character.NeedsOxygen) { return; } + float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); float prevOxygen = OxygenAmount; if (IsUnconscious) { + //clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely) + float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance); //the character dies of oxygen deprivation in 100 seconds after losing consciousness - OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f); + OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f); } else { float decreaseSpeed = -5.0f; float increaseSpeed = 10.0f; - float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); decreaseSpeed *= (1f - oxygenlowResistance); increaseSpeed *= (1f + oxygenlowResistance); + + float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier); + decreaseSpeed *= holdBreathMultiplier; OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); } @@ -1062,6 +1073,7 @@ namespace Barotrauma } if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } + if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; } if (ignoreHiddenAfflictions) { @@ -1217,6 +1229,7 @@ namespace Barotrauma var affliction = kvp.Key; var limbHealth = kvp.Value; if (affliction.Strength <= 0.0f || limbHealth != null) { continue; } + if (kvp.Key.Prefab.ResetBetweenRounds) { continue; } healthElement.Add(new XElement("Affliction", new XAttribute("identifier", affliction.Identifier), new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index a71d1e73e..03fb3ad7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -79,27 +79,35 @@ namespace Barotrauma public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; - public DamageModifier(XElement element, string parentDebugName) + public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true) { Deserialize(element); if (element.Attribute("afflictionnames") != null) { DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); } - foreach (var afflictionType in parsedAfflictionTypes) + if (checkErrors) { - if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + foreach (var afflictionType in parsedAfflictionTypes) { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); - } - } - foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) - { - if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) - { - createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + if (!AfflictionPrefab.Prefabs.Any(p => p.AfflictionType == afflictionType)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions of the type \"{afflictionType}\". Did you mean to use an affliction identifier instead?"); + } + } + foreach (var afflictionIdentifier in parsedAfflictionIdentifiers) + { + if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Could not find any afflictions with the identifier \"{afflictionIdentifier}\". Did you mean to use an affliction type instead?"); + } + } + if (!parsedAfflictionTypes.Any() && !parsedAfflictionIdentifiers.Any()) + { + createWarningOrError($"Potentially invalid damage modifier in \"{parentDebugName}\". Neither affliction types of identifiers defined."); } } + static void createWarningOrError(string msg) { #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index e3d7bedf4..4300e0da6 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,9 @@ namespace Barotrauma public Identifier[] PreferredOutpostModuleTypes { get; protected set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier Faction { get; set; } + public XElement Element { get; protected set; } @@ -97,6 +126,11 @@ namespace Barotrauma this.NpcSetIdentifier = npcSetIdentifier; } + public IEnumerable GetTags() + { + return tags; + } + public IEnumerable GetModuleFlags() { return moduleFlags; @@ -177,13 +211,23 @@ 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); } 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; } 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 030e232b1..9c7b7501e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -637,13 +637,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); @@ -778,6 +778,7 @@ namespace Barotrauma { var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character); attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter); + newAffliction = abilityAfflictionCharacter.Affliction; } if (applyAffliction) { @@ -896,6 +897,12 @@ namespace Barotrauma { reEnableTimer = duration; } +#if CLIENT + if (Hidden && LightSource != null) + { + LightSource.Enabled = false; + } +#endif } public void ReEnable() @@ -1060,7 +1067,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) @@ -1189,12 +1196,30 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); + statusEffect.AddNearbyTargets(WorldPosition, targets); statusEffect.Apply(actionType, deltaTime, character, targets); } else { - if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + + if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory) + { + foreach (Item item in inventory.AllItems) + { + if (statusEffect.TargetIdentifiers != null && + !statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) && + statusEffect.TargetIdentifiers.None(id => item.HasTag(id))) + { + continue; + } + if (statusEffect.TargetSlot > -1) + { + if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; } + } + targets.Add(item); + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); } @@ -1237,7 +1262,8 @@ namespace Barotrauma } private float blinkTimer; - private float blinkPhase; + public float BlinkPhase; + public bool FreezeBlinkState; private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; @@ -1250,16 +1276,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); @@ -1267,15 +1302,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 5b45d8882..58217cc32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -50,9 +50,15 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable] + public bool IsMachine { get; set; } + [Serialize(false, IsPropertySaveable.No), Editable] 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; } @@ -606,6 +612,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; } @@ -649,7 +658,7 @@ namespace Barotrauma if (HasTag(tag)) { target = null; - DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); + DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!"); return false; } else @@ -814,10 +823,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/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 1b3564d0f..3ce84413a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -1,8 +1,5 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -34,6 +31,7 @@ namespace Barotrauma.Abilities Alive = 4, Monster = 5, InFriendlySubmarine = 6, + Large = 7, }; protected List ParseTargetTypes(string[] targetTypeStrings) @@ -41,8 +39,7 @@ namespace Barotrauma.Abilities List targetTypes = new List(); foreach (string targetTypeString in targetTypeStrings) { - TargetType targetType = TargetType.Any; - if (!Enum.TryParse(targetTypeString, true, out targetType)) + if (!Enum.TryParse(targetTypeString, true, out TargetType targetType)) { DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); } @@ -83,6 +80,9 @@ namespace Barotrauma.Abilities return !targetCharacter.IsHuman; case TargetType.InFriendlySubmarine: return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID; + case TargetType.Large: + // mass of mudraptor is ~48 + return targetCharacter.AnimController is { Mass: > 50.0f }; default: return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 43a16839d..0ac951fd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -8,11 +9,13 @@ namespace Barotrauma.Abilities { private readonly List targetTypes; - private List conditionals = new List(); + private readonly List conditionals = new List(); public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)); + targetTypes = ParseTargetTypes( + conditionElement.GetAttributeStringArray("targettypes", + conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); foreach (XElement subElement in conditionElement.Elements()) { @@ -28,13 +31,18 @@ namespace Barotrauma.Abilities break; } } + + if (!targetTypes.Any() && !conditionals.Any()) + { + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character."); + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityCharacter abilityCharacter) { - if (!(abilityCharacter.Character is Character character)) { return false; } + if (abilityCharacter.Character is not Character character) { return false; } if (!IsViableTarget(targetTypes, character)) { return false; } foreach (var conditional in conditionals) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs new file mode 100644 index 000000000..a79830e5b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs @@ -0,0 +1,19 @@ +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData + { + private readonly Identifier identifier; + + public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter ability) { return false; } + + return !ability.Character.MarkedAsLooted.Contains(identifier); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs new file mode 100644 index 000000000..2809f3546 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs @@ -0,0 +1,16 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData + { + public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter targetCharacter) { return false; } + + return targetCharacter.Character.IsUnconscious; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 5811b3d66..ae8597e64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -13,6 +12,11 @@ namespace Barotrauma.Abilities { identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + + if (!identifiers.Any() && !tags.Any()) + { + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined."); + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index 9e85d367e..c3d2e872e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Abilities { private readonly bool? hasOutpost; private readonly Identifier[] locationIdentifiers; + private readonly bool isPositiveReputation; public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -16,12 +17,20 @@ namespace Barotrauma.Abilities hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty()); + + isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { if (abilityObject is IAbilityLocation abilityLocation) { + if (isPositiveReputation) + { + if (abilityLocation.Location?.Reputation is not { } reputation) { return false; } + if (reputation.Value <= 0) { return false; } + } + if (locationIdentifiers.Any()) { if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 0e19ec19e..6aff8ee8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -1,38 +1,53 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionMission : AbilityConditionData { - private readonly MissionType missionType; + private readonly ImmutableHashSet missionType; + private readonly ImmutableHashSet factions; + public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); - if (!Enum.TryParse(missionTypeString, out missionType)) + string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!; + HashSet missionTypes = new HashSet(); + factions = conditionElement.GetAttributeIdentifierImmutableHashSet("faction", ImmutableHashSet.Empty); + + foreach (string missionTypeString in missionTypeStrings) { - DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type."); - return; - } - if (missionType == MissionType.None) - { - DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none."); - return; + if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None) + { + if (factions.IsEmpty) + { + DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type."); + } + continue; + } + + missionTypes.Add(parsedMission); } + + missionType = missionTypes.ToImmutableHashSet(); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityMission)?.Mission is Mission mission) + if (abilityObject is IAbilityMission { Mission: { } mission }) { - return mission.Prefab.Type == missionType; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); - return false; + if (factions.Any()) + { + // FIXME there's probably a better way to check the faction affiliated with the mission later + return mission.ReputationRewards.Keys.Any(factionIdentifier => factions.Contains(factionIdentifier)); + } + + return missionType.Contains(mission.Prefab.Type); } + + LogAbilityConditionError(abilityObject, typeof(IAbilityMission)); + return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index d3ece3bf8..c27404723 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs new file mode 100644 index 000000000..e877c657b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless + { + private enum NearbyCharacterTruthy + { + OneCharacterMatches, + NoCharacterMatches + } + + private readonly NearbyCharacterTruthy truthyWhen; + private readonly float distance; + + public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches); + distance = conditionElement.GetAttributeFloat("distance", 10f); + } + + protected override bool MatchesConditionSpecific() + { + bool trueCondition = truthyWhen switch + { + NearbyCharacterTruthy.OneCharacterMatches => true, + NearbyCharacterTruthy.NoCharacterMatches => false, + _ => throw new ArgumentOutOfRangeException(nameof(truthyWhen)) + }; + + foreach (Character ally in Character.GetFriendlyCrew(character)) + { + if (ally == character) { continue; } + + float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition); + + if (distanceToCharacter < distance * distance) + { + return trueCondition; + } + } + + return !trueCondition; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs new file mode 100644 index 000000000..09fa7aaf4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs @@ -0,0 +1,22 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless + { + public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (c.IsUnconscious) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 04d7ebf62..8b20847b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -1,9 +1,5 @@ -using System; -using Barotrauma.Items.Components; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; +using Barotrauma.Extensions; +using System; namespace Barotrauma.Abilities { @@ -22,7 +18,7 @@ namespace Barotrauma.Abilities { if (tags.None()) { - return character.GetEquippedItem(null) is Item; + return character.GetEquippedItem(null) != null; } if (requireAll) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs new file mode 100644 index 000000000..f14552583 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionHasLevel : AbilityConditionDataless + { + private readonly Option matchedLevel; + private readonly Option minLevel; + + public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0 + ? Option.Some(match) + : Option.None(); + + minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0 + ? Option.Some(min) + : Option.None(); + + if (matchedLevel.IsNone() && minLevel.IsNone()) + { + throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute."); + } + } + + protected override bool MatchesConditionSpecific() + { + if (matchedLevel.TryUnwrap(out int match)) + { + return character.Info.GetCurrentLevel() == match; + } + + if (minLevel.TryUnwrap(out int min)) + { + return character.Info.GetCurrentLevel() >= min; + } + + return false; + } + } +} \ No newline at end of file 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 344a580f2..c4017a87f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -1,13 +1,11 @@ -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionHasPermanentStat : AbilityConditionDataless { private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float min; + private readonly PermanentStatPlaceholder placeholder; public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { @@ -19,11 +17,14 @@ namespace Barotrauma.Abilities string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); min = conditionElement.GetAttributeFloat("min", 0f); + placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } protected override bool MatchesConditionSpecific() { - return character.Info.GetSavedStatValue(statType, statIdentifier) >= min; + Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier); + + return character.Info.GetSavedStatValue(statType, identifier) >= min; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs new file mode 100644 index 000000000..de2f98107 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs @@ -0,0 +1,19 @@ + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasTalent : AbilityConditionDataless + { + private readonly Identifier talentIdentifier; + + public AbilityConditionHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific() + { + bool result = character.HasTalent(talentIdentifier); + return result; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs new file mode 100644 index 000000000..5c6c201de --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Collections.Immutable; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless +{ + private readonly ImmutableHashSet tags; + + public AbilityConditionHoldingItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + tags = conditionElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override bool MatchesConditionSpecific() + { + if (tags.Count is 0) + { + return HasItemInHand(character, null); + } + + foreach (Identifier tag in tags) + { + if (HasItemInHand(character, tag)) { return true; } + } + + return false; + + static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null || + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null; + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs new file mode 100644 index 000000000..3cf37c2b9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless + { + public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + int ownLevel = character.Info.GetCurrentLevel(); + + foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (crew == character) { continue; } + + if (crew.Info.GetCurrentLevel() < ownLevel) { return false; } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs new file mode 100644 index 000000000..5d0acdbf0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDataless +{ + private readonly float distance; + private readonly int count; + private readonly ImmutableHashSet targetTypes; + + public AbilityConditionNearbyCharacterCount(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + distance = conditionElement.GetAttributeFloat("distance", 10f); + count = conditionElement.GetAttributeInt("count", 1); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)).ToImmutableHashSet(); + } + + protected override bool MatchesConditionSpecific() + { + int amountNeeded = count; + foreach (Character otherCharacter in Character.CharacterList) + { + if (character.Submarine != otherCharacter.Submarine) { continue; } + if (!IsViableTarget(targetTypes, otherCharacter)) { continue; } + + if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance) + { + amountNeeded--; + + if (amountNeeded <= 0) { return true; } + } + } + + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 9d1e093b2..698e7f207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -15,5 +15,4 @@ namespace Barotrauma.Abilities } public Character Character { get; set; } } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 044d960a9..c4d73f406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -67,7 +67,7 @@ namespace Barotrauma.Abilities if (abilityObject is null) { ApplyEffect(); - } + } else { ApplyEffect(abilityObject); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs new file mode 100644 index 000000000..c7ef4461e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs @@ -0,0 +1,35 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectToNonHumans : CharacterAbilityApplyStatusEffects + { + private readonly float maxDistance; + + public CharacterAbilityApplyStatusEffectToNonHumans(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + } + + protected override void ApplyEffect() + { + foreach (Character character in Character.CharacterList) + { + if (character.IsHuman) { continue; } + + if (maxDistance < float.MaxValue) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } + } + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 5711c0ed5..969c9c5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Abilities readonly List targets = new List(); + private bool effectBeingApplied; + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); @@ -29,44 +31,57 @@ namespace Barotrauma.Abilities protected void ApplyEffectSpecific(Character targetCharacter) { - foreach (var statusEffect in statusEffects) + //prevent an infinite loop if an effect triggers itself + //(e.g. a talent that triggers when an affliction is applied, and applies that same affliction) + if (effectBeingApplied) { return; } + + effectBeingApplied = true; + + try { - if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) + foreach (var statusEffect in statusEffects) { - // currently used to spawn items on the targeted character - statusEffect.SetUser(targetCharacter); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); - if (!nearbyCharactersAppliesToSelf) + if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - targets.RemoveAll(c => c == Character); + // currently used to spawn items on the targeted character + statusEffect.SetUser(targetCharacter); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); } - if (!nearbyCharactersAppliesToAllies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + targets.Clear(); + statusEffect.AddNearbyTargets(targetCharacter.WorldPosition, targets); + if (!nearbyCharactersAppliesToSelf) + { + targets.RemoveAll(c => c == Character); + } + if (!nearbyCharactersAppliesToAllies) + { + targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + } + if (!nearbyCharactersAppliesToEnemies) + { + targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + } + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } - if (!nearbyCharactersAppliesToEnemies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { - targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + else + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); - } - else - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } } + finally + { + effectBeingApplied = false; + } } protected override void ApplyEffect() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 70b871963..83d56c28d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; namespace Barotrauma.Abilities { @@ -6,11 +7,15 @@ namespace Barotrauma.Abilities { private readonly bool allowSelf; private readonly float maxDistance = float.MaxValue; + private readonly bool inSameRoom; + private readonly ImmutableHashSet jobIdentifiers; public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + inSameRoom = abilityElement.GetAttributeBool("insameroom", false); + jobIdentifiers = abilityElement.GetAttributeIdentifierImmutableHashSet("jobs", ImmutableHashSet.Empty); } @@ -19,6 +24,27 @@ namespace Barotrauma.Abilities foreach (Character character in Character.GetFriendlyCrew(Character)) { if (!allowSelf && character == Character) { continue; } + + if (!jobIdentifiers.IsEmpty) + { + bool hadJob = false; + foreach (Identifier job in jobIdentifiers) + { + if (character.HasJob(job.Value)) + { + hadJob = true; + break; + } + } + + if (!hadJob) { continue; } + } + + if (inSameRoom && !character.IsInSameRoomAs(Character)) + { + continue; + } + if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs new file mode 100644 index 000000000..27d4afe94 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -0,0 +1,63 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectsToApprenticeship : CharacterAbilityApplyStatusEffects + { + private readonly bool invert; + private readonly ImmutableHashSet jobPrefabList = JobPrefab.Prefabs.ToImmutableHashSet(); + + public CharacterAbilityApplyStatusEffectsToApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + invert = abilityElement.GetAttributeBool("invert", false); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList); + if (apprenticeJob is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + JobPrefab? characterJob = character.Info?.Job?.Prefab; + if (characterJob is null) { continue; } + + switch (characterJob.Identifier == apprenticeJob.Identifier) + { + case true when invert: + continue; + case false when !invert: + continue; + } + + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + + public static JobPrefab? GetApprenticeJob(Character character, IReadOnlyCollection jobList) + { + foreach (JobPrefab prefab in jobList) + { + if (character.Info.GetSavedStatValue(StatTypes.Apprenticeship, prefab.Identifier) > 0) + { + return prefab; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 1204769de..4161a881e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -6,12 +6,15 @@ namespace Barotrauma.Abilities class CharacterAbilityGainSimultaneousSkill : CharacterAbility { private readonly Identifier skillIdentifier; - private readonly bool ignoreAbilitySkillGain; + + private readonly bool ignoreAbilitySkillGain, + targetAllies; public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); + targetAllies = abilityElement.GetAttributeBool("targetallies", false); } protected override void ApplyEffect(AbilityObject abilityObject) @@ -19,7 +22,20 @@ namespace Barotrauma.Abilities if (abilityObject is AbilitySkillGain abilitySkillGain) { if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } - Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true); + Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier; + + if (targetAllies) + { + foreach (Character character in Character.GetFriendlyCrew(Character)) + { + if (character == Character) { continue; } + Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } + } + else + { + Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs new file mode 100644 index 000000000..5b7d50aa0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -0,0 +1,35 @@ +namespace Barotrauma.Abilities; + +internal sealed class CharacterAbilityGiveExperience : CharacterAbility +{ + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly int amount; + + public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + private void ApplyEffectSpecific(Character targetCharacter) + { + targetCharacter.Info?.GiveExperience(amount); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffectSpecific(Character); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs new file mode 100644 index 000000000..4fe2c1c86 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStat : CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + + public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityItem ability) { return; } + + ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs new file mode 100644 index 000000000..1b4f880f8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -0,0 +1,41 @@ +#nullable enable + +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStatToTags: CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + private readonly ImmutableHashSet tags; + + public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect() + { + if (Character?.Submarine is null) { return; } + + foreach (Item item in Character.Submarine.GetItems(true)) + { + if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) + { + item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index a0750d5d4..bd54f9413 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -1,11 +1,17 @@ -using Barotrauma.Extensions; -using System.Xml.Linq; +using System; namespace Barotrauma.Abilities { + public enum PermanentStatPlaceholder + { + None, + LocationName, + LocationIndex + } + class CharacterAbilityGivePermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float value; private readonly float maxValue; @@ -13,6 +19,7 @@ namespace Barotrauma.Abilities private readonly bool removeOnDeath; private readonly bool giveOnAddingFirstTime; private readonly bool setValue; + private readonly PermanentStatPlaceholder placeholder; //private readonly float maximumValue; public override bool AllowClientSimulation => true; @@ -20,7 +27,7 @@ namespace Barotrauma.Abilities public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); @@ -29,6 +36,7 @@ namespace Barotrauma.Abilities removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); + placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } public override void InitializeAbility(bool addingFirstTime) @@ -51,14 +59,33 @@ namespace Barotrauma.Abilities private void ApplyEffectSpecific() { + Identifier identifier = HandlePlaceholders(placeholder, statIdentifier); if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue)); + foreach (Character c in Character.GetFriendlyCrew(Character)) + { + c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + } } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } + + public static Identifier HandlePlaceholders(PermanentStatPlaceholder placeholder, Identifier original) + { + if (GameMain.GameSession?.Campaign?.Map is not { } map) { return original; } + + switch (placeholder) + { + case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location: + return original.Replace("[placeholder]", location.Name); + case PermanentStatPlaceholder.LocationIndex: + return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString()); + } + + return original; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs new file mode 100644 index 000000000..37fb09935 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveReputation : CharacterAbility + { + private readonly Identifier factionIdentifier; + private readonly float amount; + + public CharacterAbilityGiveReputation(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + amount = abilityElement.GetAttributeFloat("amount", 0f); + } + + protected override void ApplyEffect() + { + if (GameMain.GameSession?.Campaign is not { } campaign) { return; } + + foreach (Faction faction in campaign.Factions) + { + if (faction.Prefab.Identifier != factionIdentifier) { continue; } + + faction.Reputation.AddReputation(amount); + break; + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) => ApplyEffect(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs new file mode 100644 index 000000000..130b5b988 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPointsToAllies.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveTalentPointsToAllies : CharacterAbility + { + private readonly int amount; + + public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.Info is null) { return; } + character.Info.AdditionalTalentPoints += amount; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs new file mode 100644 index 000000000..c9460ff46 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityMarkAsLooted: CharacterAbility + { + private readonly Identifier identifier; + public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; } + + character.MarkedAsLooted.Add(identifier); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 97b0302bd..6c275b1c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -1,30 +1,36 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyAffliction : CharacterAbility { - private readonly string[] afflictionIdentifiers; + private readonly Identifier[] afflictionIdentifiers; + + private readonly Identifier replaceWith; private readonly float addedMultiplier; public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); + afflictionIdentifiers = abilityElement.GetAttributeIdentifierArray("afflictionidentifiers", System.Array.Empty()); + replaceWith = abilityElement.GetAttributeIdentifier("replacewith", Identifier.Empty); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + var abilityAffliction = abilityObject as IAbilityAffliction; + if (abilityAffliction?.Affliction is Affliction affliction) { - foreach (string afflictionIdentifier in afflictionIdentifiers) + foreach (Identifier afflictionIdentifier in afflictionIdentifiers) { - if (affliction.Identifier == afflictionIdentifier) + if (affliction.Identifier != afflictionIdentifier) { continue; } + affliction.Strength *= 1 + addedMultiplier; + if (!replaceWith.IsEmpty) { - affliction.Strength *= 1 + addedMultiplier; - } + if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab)) + { + abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength); + } + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index a6141b79f..686aa6d48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs new file mode 100644 index 000000000..14a84d337 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -0,0 +1,27 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityReduceAffliction : CharacterAbility + { + private readonly Identifier afflictionId; + private readonly float amount; + + public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); + amount = abilityElement.GetAttributeFloat("amount", 0); + + if (afflictionId.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter character) { return; } + character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs new file mode 100644 index 000000000..78d7f501a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -0,0 +1,19 @@ +#nullable enable + +using Barotrauma.Items.Components; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility + { + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } + + int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); + ingredients.Items.RemoveAt(randomIndex); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 1660c54e1..df57f5f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -1,16 +1,15 @@ -using System.Xml.Linq; - + namespace Barotrauma.Abilities { class CharacterAbilityResetPermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; public override bool AppliesEffectOnIntervalUpdate => true; public override bool AllowClientSimulation => true; public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); } protected override void ApplyEffect(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs new file mode 100644 index 000000000..a8da25c74 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilitySetMetadataInt : CharacterAbility + { + private readonly Identifier identifier; + private readonly int value; + + public CharacterAbilitySetMetadataInt(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + value = abilityElement.GetAttributeInt("value", 0); + } + + public override void InitializeAbility(bool addingFirstTime) + { + ApplyEffect(); + } + + protected override void ApplyEffect() + { + if (identifier == Identifier.Empty) { return; } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is not { } metadata) { return; } + + metadata.SetValue(identifier, value); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs deleted file mode 100644 index e8d0ad788..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Barotrauma.Abilities -{ - class CharacterAbilityUnlockTree : CharacterAbility - { - public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - } - - public override void InitializeAbility(bool addingFirstTime) - { - if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier)); - if (subTree == null) { return; } - - subTree.ForceUnlock = true; - if (!addingFirstTime) { return; } - - foreach (var talentId in subTree.AllTalentIdentifiers) - { - if (talentId == CharacterTalent.Prefab.Identifier) { continue; } - if (Character.GiveTalent(talentId)) - { - Character.Info.AdditionalTalentPoints++; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs new file mode 100644 index 000000000..96bdd50a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Extensions; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility + { + public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + + JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); + if (apprentice is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + + HashSet> talentsTrees = new HashSet>(); + foreach (TalentSubTree subTree in talentTree.TalentSubTrees) + { + if (subTree.Type != TalentTreeType.Specialization) { continue; } + talentsTrees.Add(subTree.AllTalentIdentifiers); + } + + ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); + + foreach (Identifier identifier in selectedTalentTree) + { + if (Character.HasTalent(identifier)) { continue; } + if (Character.GiveTalent(identifier)) + { + Character.Info.AdditionalTalentPoints++; + } + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index bd4b45998..766b48ea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -18,12 +18,15 @@ namespace Barotrauma.Abilities protected readonly int maxTriggerCount; protected int timesTriggered = 0; - - // add support for OR conditions? + // add support for OR conditions? protected readonly List abilityConditions = new List(); - // separate dictionaries for each type of characterability? - protected readonly List characterAbilities = new List(); + /// + /// List of abilities that are triggered by this group. + /// Fallback abilities are triggered if the conditional fails + /// + protected readonly List characterAbilities = new List(), + fallbackAbilities = new List(); public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { @@ -38,6 +41,9 @@ namespace Barotrauma.Abilities case "abilities": LoadAbilities(subElement); break; + case "fallbackabilities": + LoadFallbackAbilities(subElement); + break; case "conditions": LoadConditions(subElement); break; @@ -47,10 +53,23 @@ namespace Barotrauma.Abilities public void ActivateAbilityGroup(bool addingFirstTime) { + if (!CheckActivatingCondition()) { return; } + foreach (var characterAbility in characterAbilities) { characterAbility.InitializeAbility(addingFirstTime); } + + foreach (var characterAbility in fallbackAbilities) + { + characterAbility.InitializeAbility(addingFirstTime); + } + } + + private bool CheckActivatingCondition() + { + if (AbilityEffectType is not AbilityEffectType.None) { return true; } + return !abilityConditions.Any(static abilityCondition => !abilityCondition.MatchesCondition()); } public void LoadConditions(ContentXElement conditionElements) @@ -85,6 +104,17 @@ namespace Barotrauma.Abilities characterAbilities.Add(characterAbility); } + public void AddFallbackAbility(CharacterAbility characterAbility) + { + if (characterAbility == null) + { + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + fallbackAbilities.Add(characterAbility); + } + // XML private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { @@ -135,6 +165,14 @@ namespace Barotrauma.Abilities } } + private void LoadFallbackAbilities(ContentXElement abilityElements) + { + foreach (var abilityElementGroup in abilityElements.Elements()) + { + AddFallbackAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); + } + } + private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent) { CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index b77f4332d..b9418955f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,29 +1,38 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { - public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { } public void CheckAbilityGroup(AbilityObject abilityObject) { if (!IsActive) { return; } - if (IsApplicable(abilityObject)) + + if (IsOverTriggerCount) { return; } + + List abilities = IsApplicable(abilityObject) ? characterAbilities : fallbackAbilities; + + foreach (CharacterAbility characterAbility in abilities) { - foreach (var characterAbility in characterAbilities) + if (characterAbility.IsViable()) { - if (characterAbility.IsViable()) - { - characterAbility.ApplyAbilityEffect(abilityObject); - } + characterAbility.ApplyAbilityEffect(abilityObject); } + } + + if (abilities.Count > 0) + { timesTriggered++; } } + private bool IsOverTriggerCount => timesTriggered >= maxTriggerCount; + private bool IsApplicable(AbilityObject abilityObject) { - if (timesTriggered >= maxTriggerCount) { return false; } foreach (var abilityCondition in abilityConditions) { if (!abilityCondition.MatchesCondition(abilityObject)) @@ -31,7 +40,8 @@ return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 7cc1e24eb..cf1100db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,4 +1,7 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -9,48 +12,72 @@ private float effectDelayTimer; - public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) - { + { // too many overlapping intervals could cause hitching? maybe randomize a little interval = abilityElementGroup.GetAttributeFloat("interval", 0f); effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f); } + public void UpdateAbilityGroup(float deltaTime) { if (!IsActive) { return; } - TimeSinceLastUpdate += deltaTime; - if (TimeSinceLastUpdate >= interval) - { - bool conditionsMatched = IsApplicable(); - effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f; - conditionsMatched &= effectDelayTimer >= effectDelay; - foreach (var characterAbility in characterAbilities) - { - if (characterAbility.IsViable()) - { - characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); - } - } - if (conditionsMatched) - { - timesTriggered++; - } - TimeSinceLastUpdate = 0; + TimeSinceLastUpdate += deltaTime; + if (TimeSinceLastUpdate < interval) { return; } + + bool conditionsMatched; + + if (AllConditionsMatched()) + { + effectDelayTimer += TimeSinceLastUpdate; + bool shouldApplyDelayedEffect = effectDelayTimer >= effectDelay; + conditionsMatched = shouldApplyDelayedEffect; } + else + { + effectDelayTimer = 0f; + conditionsMatched = false; + } + + bool hasFallbacks = fallbackAbilities.Count > 0; + + List abilitiesToRun = + !conditionsMatched && hasFallbacks + ? fallbackAbilities + : characterAbilities; + + if (hasFallbacks) + { + conditionsMatched = true; + } + + foreach (var characterAbility in abilitiesToRun) + { + if (!characterAbility.IsViable()) { continue; } + + characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); + } + + if (conditionsMatched) + { + timesTriggered++; + } + + TimeSinceLastUpdate = 0; } - private bool IsApplicable() + + private bool AllConditionsMatched() { if (timesTriggered >= maxTriggerCount) { return false; } + foreach (var abilityCondition in abilityConditions) { - if (!abilityCondition.MatchesCondition()) - { - return false; - } + if (!abilityCondition.MatchesCondition()) { return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 0cf4b3420..4cc374e79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -19,6 +19,7 @@ namespace Barotrauma // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well public List UnlockedRecipes { get; } = new List(); + public List UnlockedStoreItems { get; } = new List(); public CharacterTalent(TalentPrefab talentPrefab, Character character) { @@ -45,7 +46,17 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier); + DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}"); + } + break; + case "addedstoreitem": + if (subElement.GetAttributeIdentifier("itemtag", Identifier.Empty) is { IsEmpty: false } storeItemTag) + { + UnlockedStoreItems.Add(storeItemTag); + } + else + { + DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index d8f954873..d3cc70e56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework; +#endif namespace Barotrauma { @@ -14,6 +14,10 @@ namespace Barotrauma public readonly Sprite Icon; +#if CLIENT + public readonly Option ColorOverride; +#endif + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); public ContentXElement ConfigElement @@ -28,8 +32,22 @@ namespace Barotrauma DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); - Description = ""; - + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); + if (!nameIdentifier.IsEmpty) + { + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); + } + + Description = string.Empty; + +#if CLIENT + Color colorOverride = element.GetAttributeColor("coloroverride", Color.TransparentBlack); + + ColorOverride = colorOverride != Color.TransparentBlack + ? Option.Some(colorOverride) + : Option.None(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index e3379314f..1baec573e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -5,9 +5,9 @@ using System.Linq; namespace Barotrauma { - class TalentTree : Prefab + internal sealed class TalentTree : Prefab { - public enum TalentTreeStageState + public enum TalentStages { Invalid, Locked, @@ -40,16 +40,17 @@ namespace Barotrauma DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - + List subTrees = new List(); foreach (var subTreeElement in element.GetChildElements("subtree")) { subTrees.Add(new TalentSubTree(subTreeElement)); } + TalentSubTrees = subTrees.ToImmutableArray(); AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet(); } - + public bool TalentIsInTree(Identifier talentIdentifier) { return AllTalentIdentifiers.Contains(talentIdentifier); @@ -57,31 +58,44 @@ namespace Barotrauma public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) { - return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection)Array.Empty()); + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection)Array.Empty()); + } + + public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection selectedTalents) + { + IEnumerable blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)), + requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier)); + + return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements + !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one } // i hate this function - markus // me too - joonas - public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) + public static TalentStages GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection selectedTalents) { - if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } + if (character?.Info?.Job.Prefab is null) { return TalentStages.Invalid; } - if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentStages.Invalid; } - TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + if (subTree is null) { return TalentStages.Invalid; } - if (subTree == null) { return TalentTreeStageState.Invalid; } + if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) + { + return TalentStages.Locked; + } TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t))) + if (targetTalentOption.HasEnoughTalents(character.Info)) { - return TalentTreeStageState.Unlocked; + return TalentStages.Unlocked; } - if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t))) + if (targetTalentOption.HasSelectedTalent(selectedTalents)) { - return TalentTreeStageState.Highlighted; + return TalentStages.Highlighted; } bool hasTalentInLastTier = true; @@ -91,55 +105,51 @@ namespace Barotrauma if (lastindex >= 0) { TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex]; - hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent); - isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t)); + hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents); + isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info); } if (!hasTalentInLastTier) { - return TalentTreeStageState.Locked; + return TalentStages.Locked; } bool hasPointsForNewTalent = character.Info.GetTotalTalentPoints() - selectedTalents.Count > 0; if (hasPointsForNewTalent) { - return isLastTalentPurchased ? TalentTreeStageState.Highlighted : TalentTreeStageState.Available; + return isLastTalentPurchased ? TalentStages.Highlighted : TalentStages.Available; } - return TalentTreeStageState.Locked; - - bool HasTalent(Identifier talentId) - { - return selectedTalents.Contains(talentId); - } + return TalentStages.Locked; } - public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection selectedTalents) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) { if (character?.Info?.Job.Prefab == null) { return false; } - if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } + + if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } - foreach (var subTree in talentTree.TalentSubTrees) + foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; } + if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return false; } + } + foreach (var subTree in talentTree!.TalentSubTrees) + { foreach (var talentOptionStage in subTree.TalentOptionStages) { - bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t)); + bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents); if (!hasTalentInThisTier) { if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) { - return true; - } - else - { - break; + return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); } + break; } } } @@ -164,60 +174,130 @@ namespace Barotrauma } } } + return viableTalents; } public override void Dispose() { } } - class TalentSubTree + internal enum TalentTreeType + { + Specialization, + Primary + } + + internal sealed class TalentSubTree { public Identifier Identifier { get; } public LocalizedString DisplayName { get; } - public bool ForceUnlock; - public readonly ImmutableArray TalentOptionStages; public readonly ImmutableHashSet AllTalentIdentifiers; + public readonly TalentTreeType Type; + public readonly ImmutableHashSet RequiredTrees; + public readonly ImmutableHashSet BlockedTrees; + + public bool IsCompleted(IReadOnlyCollection talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents)); + public bool HasAnyTalent(IReadOnlyCollection talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents)); + public TalentSubTree(ContentXElement subTreeElement) { Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); + string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty); + if (string.IsNullOrWhiteSpace(nameIdentifier)) + { + nameIdentifier = $"talenttree.{Identifier}"; + } + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); + Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization); + RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet.Empty); + BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet.Empty); List talentOptionStages = new List(); foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } + TalentOptionStages = talentOptionStages.ToImmutableArray(); AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet(); } - } - class TalentOption + internal readonly struct TalentOption { private readonly ImmutableHashSet talentIdentifiers; public IEnumerable TalentIdentifiers => talentIdentifiers; - public bool HasTalent(Identifier talentIdentifier) + public readonly int MaxChosenTalents; + + /// + /// When specified the talent option will show talent with this identifier + /// and clicking on it will expand the talent option to show the talents + /// + public readonly Dictionary> ShowCaseTalents = new Dictionary>(); + + public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents; + public bool HasEnoughTalents(IReadOnlyCollection selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents; + + // No LINQ + public bool HasSelectedTalent(IReadOnlyCollection selectedTalents) { - return talentIdentifiers.Contains(talentIdentifier); + foreach (Identifier talent in selectedTalents) + { + if (talentIdentifiers.Contains(talent)) + { + return true; + } + } + return false; + } + + public int CountMatchingTalents(IReadOnlyCollection talents) + { + int i = 0; + foreach (Identifier talent in talents) + { + if (talentIdentifiers.Contains(talent)) + { + i++; + } + } + return i; } public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { - var talentIdentifiers = new HashSet(); - foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1); + + HashSet identifiers = new HashSet(); + + foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements()) { - Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); - talentIdentifiers.Add(identifier); + Identifier elementName = talentOptionElement.Name.ToIdentifier(); + if (elementName == "talentoption") + { + identifiers.Add(talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty)); + } + else if (elementName == "showcasetalent") + { + Identifier showCaseIdentifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + HashSet showCaseTalentIdentifiers = new HashSet(); + foreach (ContentXElement subElement in talentOptionElement.Elements()) + { + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + showCaseTalentIdentifiers.Add(identifier); + identifiers.Add(identifier); + } + ShowCaseTalents.Add(showCaseIdentifier, showCaseTalentIdentifiers.ToImmutableHashSet()); + } } - this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); + + talentIdentifiers = identifiers.ToImmutableHashSet(); } } - -} +} \ No newline at end of file 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/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index d99d29b57..1e2dccfad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -18,7 +18,7 @@ namespace Barotrauma public const string LocalModsDir = "LocalMods"; public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( - SaveUtil.SaveFolder, + SaveUtil.DefaultSaveFolder, "WorkshopMods", "Installed"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index b184da625..f2b9dbae6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; @@ -63,6 +65,8 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return:NotNullIfNotNull("def")] + public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs index b19d0f034..7fb3453ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -121,6 +121,11 @@ namespace Barotrauma public static bool operator !=(string str, in Identifier? identifier) => !(identifier == str); + + internal int IndexOf(char c) => Value.IndexOf(c); + + internal Identifier this[Range range] => Value[range].ToIdentifier(); + internal Char this[int i] => Value[i]; } public static class IdentifierExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index d8b9730c9..d0ac15a8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -844,7 +844,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) => @@ -1046,11 +1054,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)) { @@ -1222,7 +1225,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 { @@ -1750,6 +1753,17 @@ namespace Barotrauma NewMessage("Set minimum loading time to " + time + " seconds.", Color.White); })); + + commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null, + () => + { + if (GameMain.NetworkMember == null) { return null; } + return new string[][] + { + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + }; + })); + commands.Add(new Command("storeinfo", "", (string[] args) => { if (GameMain.GameSession?.Map?.CurrentLocation is Location location) @@ -1803,6 +1817,7 @@ namespace Barotrauma commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); @@ -2187,7 +2202,7 @@ namespace Barotrauma //Dont do a thing, random is basically Human points anyways - its in the help description. break; default: - var matchingCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray()); if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index bf8d1addd..7a673e49b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -43,6 +43,7 @@ namespace Barotrauma OnRepairComplete, OnItemFabricationSkillGain, OnItemFabricatedAmount, + OnItemFabricatedIngredients, OnAllyItemFabricatedAmount, OnOpenItemContainer, OnUseRangedWeapon, @@ -51,6 +52,7 @@ namespace Barotrauma OnSelfRagdoll, OnRagdoll, OnRoundEnd, + OnLootCharacter, OnAnyMissionCompleted, OnAllMissionsCompleted, OnGiveOrder, @@ -80,6 +82,11 @@ namespace Barotrauma // Skills ElectricalSkillBonus, HelmSkillBonus, + HelmSkillOverride, + MedicalSkillOverride, + WeaponsSkillOverride, + ElectricalSkillOverride, + MechanicalSkillOverride, MechanicalSkillBonus, MedicalSkillBonus, WeaponsSkillBonus, @@ -105,6 +112,7 @@ namespace Barotrauma RangedSpreadReduction, // Utility RepairSpeed, + MechanicalRepairSpeed, DeconstructorSpeedMultiplier, RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, @@ -115,20 +123,57 @@ namespace Barotrauma GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, SkillGainSpeed, + ExtraLevelGain, + HelmSkillGainSpeed, + WeaponsSkillGainSpeed, + MedicalSkillGainSpeed, + ElectricalSkillGainSpeed, + MechanicalSkillGainSpeed, MedicalItemApplyingMultiplier, + MedicalItemDurationMultiplier, + PoisonMultiplier, // Tinker TinkeringDuration, TinkeringStrength, TinkeringDamage, // Misc ReputationGainMultiplier, + ReputationLossMultiplier, MissionMoneyGainMultiplier, ExperienceGainMultiplier, MissionExperienceGainMultiplier, ExtraMissionCount, ExtraSpecialSalesCount, - ApplyTreatmentsOnSelfFraction, + StoreSellMultiplier, + StoreBuyMultiplierAffiliated, + StoreBuyMultiplier, + ShipyardBuyMultiplierAffiliated, + ShipyardBuyMultiplier, MaxAttachableCount, + ExplosionRadiusMultiplier, + ExplosionDamageMultiplier, + FabricateMedicineSpeedMultiplier, + BallastFloraDamageMultiplier, + HoldBreathMultiplier, + Apprenticeship, + Affiliation, + CPRBoost, + LockedTalents + } + + internal enum ItemTalentStats + { + None, + DetoriationSpeed, + BatteryCapacity, + EngineSpeed, + EngineMaxSpeed, + PumpSpeed, + PumpMaxFlow, + ReactorMaxOutput, + ReactorFuelEfficiency, + DeconstructorSpeed, + FabricationSpeed } [Flags] @@ -145,8 +190,8 @@ namespace Barotrauma GainSkillPastMaximum = 0x80, RetainExperienceForNewCharacter = 0x100, AllowSecondOrderedTarget = 0x200, - PowerfulCPR = 0x400, - AlwaysStayConscious = 0x800, + AlwaysStayConscious = 0x400, + CanNotDieToAfflictions = 0x800, } [Flags] 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 9413193d1..8313cc189 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -25,11 +25,14 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool RequireEquipped { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool Recursive { get; set; } + [Serialize(-1, IsPropertySaveable.Yes)] public int ItemContainerIndex { get; set; } private readonly IReadOnlyList conditionals; - + private readonly Identifier[] itemIdentifierSplit; private readonly Identifier[] itemTags; @@ -97,7 +100,7 @@ namespace Barotrauma { if (inventory == null) { return false; } int count = 0; - foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier))) + foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive)) { if (!ConditionalsMatch(item, character)) { continue; } count++; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs new file mode 100644 index 000000000..530a63429 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMissionAction.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckMissionAction : BinaryOptionAction +{ + public enum MissionType + { + Current, + Selected, + Available + } + + [Serialize(MissionType.Current, IsPropertySaveable.Yes)] + public MissionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MissionCount { get; set; } + + public CheckMissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MissionCount = Math.Max(MissionCount, 0); + } + + protected override bool? DetermineSuccess() + { + var missions = Type switch + { + MissionType.Current => GameMain.GameSession?.Missions, + MissionType.Selected => GameMain.GameSession?.Campaign?.Missions, + MissionType.Available => GameMain.GameSession?.Map?.CurrentLocation?.AvailableMissions, + _ => null + }; + if (missions is not null) + { + if (!MissionIdentifier.IsEmpty) + { + return missions.Any(m => m.Prefab.Identifier == MissionIdentifier); + } + else if (!MissionTag.IsEmpty) + { + return missions.Count(m => m.Prefab.Tags.Contains(MissionTag.Value)) >= MissionCount; + } + else + { + return missions.Count() >= MissionCount; + } + } + return MissionIdentifier.IsEmpty && MissionTag.IsEmpty && MissionCount == 0; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs new file mode 100644 index 000000000..15e5e90a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckObjectiveAction.cs @@ -0,0 +1,15 @@ +namespace Barotrauma; + +partial class CheckObjectiveAction : BinaryOptionAction +{ + public CheckObjectiveAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + bool success = false; + DetermineSuccessProjSpecific(ref success); + return success; + } + + partial void DetermineSuccessProjSpecific(ref bool success); +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs index 6723d3abb..bb8b8e44d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckOrderAction.cs @@ -1,7 +1,15 @@ +using Barotrauma.Extensions; + namespace Barotrauma { class CheckOrderAction : BinaryOptionAction { + public enum OrderPriority + { + Top, + Any + } + [Serialize("", IsPropertySaveable.Yes)] public Identifier TargetTag { get; set; } @@ -14,35 +22,58 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier OrderTargetTag { get; set; } + [Serialize(OrderPriority.Top, IsPropertySaveable.Yes)] + public OrderPriority Priority { get; set; } + public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { - Character targetCharacter = null; - if (!TargetTag.IsEmpty) + var targetCharacters = ParentEvent.GetTargets(TargetTag); + if (targetCharacters.None()) { - foreach (var t in ParentEvent.GetTargets(TargetTag)) - { - if (t is Character c) - { - targetCharacter = c; - break; - } - } - } - if (targetCharacter == null) - { - DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); + DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail."); return false; } - var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Identifier == OrderIdentifier) + foreach (var t in targetCharacters) { - if (!OrderTargetTag.IsEmpty) + if (t is not Character c) { - if (currentOrderInfo.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag)) { return false; } + continue; + } + if (Priority == OrderPriority.Top) + { + if (c.GetCurrentOrderWithTopPriority() is Order topPrioOrder && IsMatch(topPrioOrder)) + { + return true; + } + } + else if (Priority == OrderPriority.Any) + { + foreach (var order in c.CurrentOrders) + { + if (IsMatch(order)) + { + return true; + } + } + } + + bool IsMatch(Order order) + { + if (order?.Identifier == OrderIdentifier) + { + if (!OrderTargetTag.IsEmpty && (order.TargetEntity is not Item targetItem || !targetItem.HasTag(OrderTargetTag))) + { + return false; + } + if (OrderOption.IsEmpty || order?.Option == OrderOption) + { + return true; + } + } + return false; } - return OrderOption.IsEmpty || currentOrderInfo?.Option == OrderOption; } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs new file mode 100644 index 000000000..b0ac35616 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckPurchasedItemsAction.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; + +namespace Barotrauma; + +class CheckPurchasedItemsAction : BinaryOptionAction +{ + public enum TransactionType + { + Purchased, + Sold + } + + [Serialize(TransactionType.Purchased, IsPropertySaveable.Yes)] + public TransactionType Type { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemTag { get; set; } + + [Serialize(1, IsPropertySaveable.Yes)] + public int MinCount { get; set; } + + public CheckPurchasedItemsAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + MinCount = Math.Max(MinCount, 1); + } + + protected override bool? DetermineSuccess() + { + if (ItemIdentifier.IsEmpty && ItemTag.IsEmpty) + { + return false; + } + if (GameMain.GameSession?.Campaign?.CargoManager is not CargoManager cargoManager) + { + return false; + } + if (Type == TransactionType.Purchased) + { + int totalPurchased = 0; + foreach ((Identifier id, var items) in cargoManager.PurchasedItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalPurchased += items.Find(i => i.ItemPrefabIdentifier == ItemIdentifier)?.Quantity ?? 0; + } + else if (!ItemTag.IsEmpty) + { + foreach (var item in items) + { + if (item.ItemPrefab.Tags.Contains(ItemTag)) + { + totalPurchased += item.Quantity; + } + } + } + if (totalPurchased >= MinCount) + { + return true; + } + } + } + else + { + int totalSold = 0; + foreach ((Identifier id, var items) in cargoManager.SoldItems) + { + if (!ItemIdentifier.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Identifier == ItemIdentifier); + } + else if (!ItemTag.IsEmpty) + { + totalSold += items.Count(i => i.ItemPrefab.Tags.Contains(ItemTag)); + } + if (totalSold >= MinCount) + { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index a438db9a5..1c5c8f0b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -59,7 +59,10 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } - public Character speaker + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreInterruptDistance { get; set; } + + public Character Speaker { get; private set; @@ -124,7 +127,7 @@ namespace Barotrauma #else foreach (Client c in GameMain.Server.ConnectedClients) { - if (c.InGame && c.Character != null) { ServerWrite(speaker, c, interrupt); } + if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); } } #endif ResetSpeaker(); @@ -160,7 +163,7 @@ namespace Barotrauma selectedOption = -1; interrupt = false; dialogOpened = false; - speaker = null; + Speaker = null; } public override bool SetGoToTarget(string goTo) @@ -181,15 +184,14 @@ namespace Barotrauma private void ResetSpeaker() { - if (speaker == null) { return; } - speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.ActiveConversation = null; - speaker.SetCustomInteract(null, null); + if (Speaker == null) { return; } + Speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + Speaker.ActiveConversation = null; + Speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif - var humanAI = speaker.AIController as HumanAIController; - if (humanAI != null && !speaker.IsDead && !speaker.Removed) + if (Speaker.AIController is HumanAIController humanAI && !Speaker.IsDead && !Speaker.Removed) { humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } @@ -207,7 +209,6 @@ namespace Barotrauma public override void Update(float deltaTime) { - lastActiveTime = Timing.TotalTime; if (interrupt) { Interrupted?.Update(deltaTime); @@ -216,6 +217,7 @@ namespace Barotrauma { if (dialogOpened) { + lastActiveTime = Timing.TotalTime; #if CLIENT if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) { @@ -226,7 +228,7 @@ namespace Barotrauma Reset(); } #endif - if (ShouldInterrupt()) + if (ShouldInterrupt(requireTarget: true)) { ResetSpeaker(); interrupt = true; @@ -236,34 +238,34 @@ namespace Barotrauma if (!SpeakerTag.IsEmpty) { - if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } - speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; - if (speaker == null || speaker.Removed) + if (Speaker != null && !Speaker.Removed && Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } + Speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; + if (Speaker == null || Speaker.Removed) { return; } //some conversation already assigned to the speaker, wait for it to be removed - if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) + if (Speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && Speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } else if (!WaitForInteraction) { - TryStartConversation(speaker); + TryStartConversation(Speaker); } - else if (speaker.ActiveConversation != this) + else if (Speaker.ActiveConversation != this) { - speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; - speaker.ActiveConversation = this; + Speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; + Speaker.ActiveConversation = this; #if CLIENT - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else - speaker.SetCustomInteract( + Speaker.SetCustomInteract( TryStartConversation, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); + GameMain.NetworkMember.CreateEntityEvent(Speaker, new Character.AssignCampaignInteractionEventData()); #endif } return; @@ -275,7 +277,9 @@ namespace Barotrauma } else { - if (ShouldInterrupt()) + //after the conversation has been finished and the target character assigned, + //we no longer care if we still have a target + if (ShouldInterrupt(requireTarget: false)) { ResetSpeaker(); interrupt = true; @@ -287,35 +291,36 @@ namespace Barotrauma } } - private bool ShouldInterrupt() + private bool ShouldInterrupt(bool requireTarget) { IEnumerable targets = Enumerable.Empty(); - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget) { - targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); + targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e, requireTarget)); if (!targets.Any()) { return true; } } - if (speaker != null) + if (Speaker != null) { - if (!TargetTag.IsEmpty) + if (!TargetTag.IsEmpty && requireTarget && !IgnoreInterruptDistance) { - if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } + if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, Speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } } - if (speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) + if (Speaker.AIController is HumanAIController humanAI && !humanAI.AllowCampaignInteraction()) { return true; } - return speaker.Removed || speaker.IsDead || speaker.IsIncapacitated; + return Speaker.Removed || Speaker.IsDead || Speaker.IsIncapacitated; } return false; } - private bool IsValidTarget(Entity e) + private bool IsValidTarget(Entity e, bool requirePlayerControlled = true) { - bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && - (e == Character.Controlled || character.IsRemotePlayer); + bool isValid = + e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && + (character == Character.Controlled || character.IsRemotePlayer || !requirePlayerControlled); #if SERVER if (!dialogOpened) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index c0fc93a5b..b570a750d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -40,7 +40,8 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction."); continue; } - Actions.Add(Instantiate(scriptedEvent, e)); + var action = Instantiate(scriptedEvent, e); + if (action != null) { Actions.Add(action); } } } @@ -149,6 +150,10 @@ namespace Barotrauma ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) }); try { + if (constructor == null) + { + throw new Exception($"Error in scripted event \"{scriptedEvent.Prefab.Identifier}\" - could not find a constructor for the EventAction \"{actionType}\"."); + } return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction; } catch (Exception ex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs index 2834b8b84..6ac082e66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MessageBoxAction.cs @@ -49,6 +49,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool ObjectiveCanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + private bool isFinished = false; public MessageBoxAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 2c298853d..48413e84c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -1,8 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -14,8 +16,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 +42,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 +61,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, Barotrauma.LocationType.Prefabs[LocationTypes[0]]); unlockLocation = emptyLocation; } } @@ -84,6 +89,7 @@ namespace Barotrauma } if (unlockedMission != null) { + 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,46 +105,80 @@ namespace Barotrauma IconColor = unlockedMission.Prefab.IconColor }; #else - NotifyMissionUnlock(unlockedMission); - #endif + NotifyMissionUnlock(unlockedMission, unlockLocation); +#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() @@ -147,7 +187,7 @@ namespace Barotrauma } #if SERVER - private void NotifyMissionUnlock(Mission mission) + private static void NotifyMissionUnlock(Mission mission, Location unlockLocation) { foreach (Client client in GameMain.Server.ConnectedClients) { @@ -155,6 +195,7 @@ namespace Barotrauma outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION); outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION); outmsg.WriteIdentifier(mission.Prefab.Identifier); + outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(unlockLocation) ?? -1); outmsg.WriteString(mission.Name.Value); GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } 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..44fdc7144 --- /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 + { + 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 143563584..7905486c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,7 +1,6 @@ -using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -11,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; } @@ -21,7 +20,17 @@ namespace Barotrauma private bool isFinished = false; - public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + 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(TeamID)) + { + 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)))}."); + } + } private List affectedNpcs = null; @@ -33,42 +42,48 @@ 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)); } } void ChangeItemTeam(Submarine sub, bool allowStealing) { - foreach (Item item in npc.Inventory.AllItems) + foreach (Item item in npc.Inventory.FindAllItems(recursive: true)) { 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/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 a3aa06d7a..21f2242f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -1,9 +1,8 @@ -using System; -using Barotrauma.Extensions; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -11,6 +10,7 @@ namespace Barotrauma { public enum SpawnLocationType { + Any, MainSub, Outpost, MainPath, @@ -40,7 +40,7 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)] + [Serialize(SpawnLocationType.Any, IsPropertySaveable.Yes)] public SpawnLocationType SpawnLocation { get; set; } [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] @@ -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,8 @@ 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)); } public override bool IsFinished(ref string goTo) @@ -118,7 +120,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,11 +153,11 @@ 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); if (LootingIsStealing) @@ -151,6 +174,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 +196,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) { @@ -177,7 +208,7 @@ namespace Barotrauma } else if (!ItemIdentifier.IsEmpty) { - if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)"); } @@ -211,7 +242,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 +270,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; @@ -257,21 +288,11 @@ namespace Barotrauma { if (!SpawnPointTag.IsEmpty) { - List potentialItems = SpawnLocation switch - { - SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub), - SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null), - SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + List potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced(); + var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced(); if (target != null) { return target; } } @@ -281,19 +302,26 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } + private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine) + { + return spawnLocation switch + { + SpawnLocationType.Any => true, + SpawnLocationType.MainSub => submarine == Submarine.MainSub, + SpawnLocationType.MainPath => submarine == null, + SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, + SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } }, + SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } }, + SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null, + _ => throw new NotImplementedException(), + }; + } + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { - List potentialSpawnPoints = spawnLocation switch - { - SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null), - SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null), - SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + 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); if (moduleFlags != null && moduleFlags.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 208a7fbfa..89eff4f39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -32,11 +32,13 @@ namespace Barotrauma ("bot", v => TagBots(playerCrewOnly: false)), ("crew", v => TagCrew()), ("humanprefabidentifier", TagHumansByIdentifier), + ("jobidentifier", TagHumansByJobIdentifier), ("structureidentifier", TagStructuresByIdentifier), ("structurespecialtag", TagStructuresBySpecialTag), ("itemidentifier", TagItemsByIdentifier), ("itemtag", TagItemsByTag), - ("hullname", TagHullsByName) + ("hullname", TagHullsByName), + ("submarine", TagSubmarinesByType), }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); } @@ -93,6 +95,18 @@ namespace Barotrauma } } } + + private void TagHumansByJobIdentifier(Identifier jobIdentifier) + { + foreach (Character c in Character.CharacterList) + { + if (c.HasJob(jobIdentifier)) + { + ParentEvent.AddTarget(Tag, c); + } + } + } + private void TagStructuresByIdentifier(Identifier identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); @@ -118,6 +132,11 @@ namespace Barotrauma ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } + private void TagSubmarinesByType(Identifier type) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier())); + } + private bool SubmarineTypeMatches(Submarine sub) { if (SubmarineType == SubType.Any) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 385dc13ae..8f7a849ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -8,6 +8,12 @@ namespace Barotrauma { class TriggerAction : EventAction { + public enum TriggerType + { + Inside, + Outside + } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")] public Identifier Target1Tag { get; set; } @@ -23,7 +29,10 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")] public Identifier ApplyToTarget2 { get; set; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")] + [Serialize(TriggerType.Inside, IsPropertySaveable.Yes, description: "Determines if the targets must be inside or outside of the radius.")] + public TriggerType Type { get; set; } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range to activate the trigger.")] public float Radius { get; set; } [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] @@ -38,6 +47,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] public bool AllowMultipleTargets { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If true and using multiple targets, all targets must be inside/outside the radius.")] + public bool CheckAllTargets { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } @@ -57,6 +69,8 @@ namespace Barotrauma public bool isRunning = false; private readonly List> npcsOrItems = new List>(); + + private readonly List<(Entity e1, Entity e2)> triggerers = new List<(Entity e1, Entity e2)>(); public override void Update(float deltaTime) { @@ -66,18 +80,44 @@ namespace Barotrauma var targets1 = ParentEvent.GetTargets(Target1Tag); if (!targets1.Any()) { return; } - + + triggerers.Clear(); foreach (Entity e1 in targets1) { - if (DisableInCombat && IsInCombat(e1)) { continue; } - if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } + if (DisableInCombat && IsInCombat(e1)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (!TargetModuleType.IsEmpty) { - if (IsCloseEnoughToHull(e1, out Hull hull)) + if (!CheckAllTargets && CheckDistanceToHull(e1, out Hull hull)) { Trigger(e1, hull); return; } + else if (CheckAllTargets) + { + if (CheckDistanceToHull(e1, out hull)) + { + triggerers.Add((e1, hull)); + } + else + { + return; + } + } continue; } @@ -85,9 +125,26 @@ namespace Barotrauma foreach (Entity e2 in targets2) { - if (e1 == e2) { continue; } - if (DisableInCombat && IsInCombat(e2)) { continue; } - if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; } + if (e1 == e2) + { + continue; + } + if (DisableInCombat && IsInCombat(e2)) + { + if (CheckAllTargets) + { + return; + } + continue; + } + if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) + { + if (CheckAllTargets) + { + return; + } + continue; + } if (WaitForInteraction) { @@ -173,16 +230,35 @@ namespace Barotrauma Vector2 pos1 = e1.WorldPosition; Vector2 pos2 = e2.WorldPosition; distance = Vector2.Distance(pos1, pos2); - if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || - ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || - Vector2.DistanceSquared(pos1, pos2) < Radius * Radius) + if ((Type == TriggerType.Inside) == IsWithinRadius()) + { + if (!CheckAllTargets) + { + Trigger(e1, e2); + return; + } + else + { + triggerers.Add((e1, e2)); + } + } + else if (CheckAllTargets) { - Trigger(e1, e2); return; } + + bool IsWithinRadius() => + ((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) || + ((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) || + Vector2.DistanceSquared(pos1, pos2) < Radius * Radius; } } - } + } + + foreach (var (e1, e2) in triggerers) + { + Trigger(e1, e2); + } } private void ResetTargetIcons() @@ -205,7 +281,7 @@ namespace Barotrauma } } - private bool IsCloseEnoughToHull(Entity e, out Hull hull) + private bool CheckDistanceToHull(Entity e, out Hull hull) { hull = null; if (Radius <= 0) @@ -213,36 +289,35 @@ namespace Barotrauma if (e is Character character && character.CurrentHull != null && character.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = character.CurrentHull; - return true; + return Type == TriggerType.Inside; } else if (e is Item item && item.CurrentHull != null && item.CurrentHull.OutpostModuleTags.Contains(TargetModuleType)) { hull = item.CurrentHull; - return true; + return Type == TriggerType.Inside; } - return false; + return Type == TriggerType.Outside; } else { foreach (Hull potentialHull in Hull.HullList) { if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; } - Rectangle hullRect = potentialHull.WorldRect; hullRect.Inflate(Radius, Radius); if (Submarine.RectContains(hullRect, e.WorldPosition)) { hull = potentialHull; - return true; + return Type == TriggerType.Inside; } } - return false; + return Type == TriggerType.Outside; } } - private bool IsInCombat(Entity entity) + private static bool IsInCombat(Entity entity) { - if (!(entity is Character character)) { return false; } + if (entity is not Character character) { return false; } foreach (Character c in Character.CharacterList) { if (c.IsDead || c.Removed || c.IsIncapacitated || !c.Enabled) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs index 587fb20a7..f2128f82e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TutorialSegmentAction.cs @@ -13,6 +13,12 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public Identifier ObjectiveTag { get; set; } + [Serialize(true, IsPropertySaveable.Yes)] + public bool CanBeCompleted { get; set; } + + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ParentObjectiveId { get; set; } + [Serialize(false, IsPropertySaveable.Yes)] public bool AutoPlayVideo { get; set; } 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 8ecedce89..dfde03f6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -71,8 +71,9 @@ 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(); #if DEBUG && SERVER @@ -150,17 +151,24 @@ 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); - EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode; + EventSet initialEventSet = SelectRandomEvents( + EventSet.Prefabs.ToList(), + requireCampaignSet: playingCampaign, + random: rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + initialEventSet = SelectRandomEvents( + EventSet.Prefabs.Where(e => !e.Additive).ToList(), + requireCampaignSet: playingCampaign, + random: rand); } if (initialEventSet != null) { @@ -180,14 +188,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(); @@ -201,6 +202,7 @@ namespace Barotrauma } AddChildEvents(initialEventSet); + void AddChildEvents(EventSet eventSet) { if (eventSet == null) { return; } @@ -208,7 +210,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) @@ -351,6 +353,7 @@ namespace Barotrauma QueuedEvents.Clear(); finishedEvents.Clear(); nonRepeatableEvents.Clear(); + usedUniqueSets.Clear(); preloadedSprites.ForEach(s => s.Remove()); preloadedSprites.Clear(); @@ -364,15 +367,25 @@ namespace Barotrauma /// public void RegisterEventHistory() { - level.LevelData.EventsExhausted = true; - if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost) + if (level?.LevelData != null) { - level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); - if (level.LevelData.EventHistory.Count > MaxEventHistory) + level.LevelData.EventsExhausted = true; + if (level.LevelData.Type == LevelData.LevelType.Outpost) { - level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory); + 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); + } + level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); + } + foreach (var usedUniqueSet in usedUniqueSets) + { + if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier)) + { + level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier); + } } - level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e))); } } @@ -383,9 +396,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; } @@ -398,6 +411,11 @@ namespace Barotrauma DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); + if (eventSet.Unique && !usedUniqueSets.Contains(eventSet)) + { + usedUniqueSets.Add(eventSet); + } + int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -426,9 +444,12 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab e) - => e.BiomeIdentifier.IsEmpty || - e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + bool isPrefabSuitable(EventPrefab e) => + (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.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) { @@ -437,9 +458,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++) { @@ -496,12 +517,12 @@ namespace Barotrauma selectedEvents[eventSet].Add(newEvent); } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; + var location = GetEventLocation(); foreach (EventSet childEventSet in eventSet.ChildSets) { if (!IsValidForLevel(childEventSet, level)) { continue; } - if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } - CreateEvents(childEventSet); + if (!IsValidForLocation(childEventSet, location)) { continue; } + CreateEvents(childEventSet); } } } @@ -536,10 +557,32 @@ namespace Barotrauma } } - Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; - if (location != null) + var location = GetEventLocation(); + allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + + allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly || + (GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true })); + + int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location); + int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location); + if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex)) { - allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex); + } + else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex)) + { + allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex); + } + else + { + // When there are no forced sets, only allow sets that aren't forced at any specific location + allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0); + } + + if (allowedEventSets.Count() == 1) + { + // When there's only a single set available, just select it directly + return allowedEventSets.First(); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -558,18 +601,31 @@ namespace Barotrauma return null; } - private bool IsValidForLevel(EventSet eventSet, Level level) + private static bool IsValidForLevel(EventSet eventSet, Level level) { return level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && level.LevelData.Type == eventSet.LevelType && - (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); + (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) && + (!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier)); } private bool IsValidForLocation(EventSet eventSet, Location location) { - return eventSet.LocationTypeIdentifiers == null || - eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier); + 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; } + return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier); + } + + private Location GetEventLocation() + { + return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation; } private bool CanStartEventSet(EventSet eventSet) @@ -688,53 +744,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) { @@ -742,13 +795,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); } } 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 cd6a3cae6..27d14aed6 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,11 +111,22 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; + public readonly bool IgnoreIntensity; + public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; + /// + /// If true, events from this set shouldn't be selected again as long as they remain in which has a limited size. + /// Use to prevent selecting the whole set again altogether. + /// public readonly bool OncePerOutpost; + /// + /// If true, the whole set can only be selected once for a level. + /// + public readonly bool Unique; + public readonly bool DelayWhenCrewAway; public readonly bool TriggerEventCooldown; @@ -126,13 +138,26 @@ namespace Barotrauma public readonly float ResetTime; + /// + /// Used to force an event set based on how many other locations have been discovered before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtDiscoveredNr; + + /// + /// Used to force an event set based on how many other outposts have been visited before this. (Used for campaign tutorial event sets.) + /// + public readonly int ForceAtVisitedNr; + + public readonly bool CampaignTutorialOnly; + 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; @@ -163,6 +188,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; @@ -245,6 +272,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) { @@ -267,11 +296,21 @@ 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); OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); + Unique = element.GetAttributeBool("unique", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat("resettime", 0); + CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), false); + + ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1); + ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1); + if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0) + { + DebugConsole.ThrowError($"Error with event set \"{Identifier}\" - both ForceAtDiscoveredNr and ForceAtVisitedNr are defined, this could lead to unexpected behavior"); + } DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); foreach (var subElement in element.Elements()) @@ -309,15 +348,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; } @@ -342,8 +383,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) @@ -489,6 +544,11 @@ namespace Barotrauma } } + public override string ToString() + { + return $"{base.ToString()} ({Identifier.Value})"; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 7e4da0ea5..85ad5a4ff 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); @@ -214,8 +217,9 @@ 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)); @@ -226,8 +230,16 @@ namespace Barotrauma bool requiresRescue = element.GetAttributeBool("requirerescue", false); - Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true); - + var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None); + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true); + 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) { spawnedCharacter.GiveIdCardTags(wp); @@ -237,7 +249,10 @@ namespace Barotrauma { requireRescue.Add(spawnedCharacter); #if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + if (allowOrderingRescuees) + { + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); + } #endif } 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 6e46b3c5b..5b1f68213 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..b3af7964b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EndMission.cs @@ -0,0 +1,290 @@ +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 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); + + 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) + { + int projectileAmount = Rand.Range(3, 6); + float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)); + 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(); + projectile.Use(); + }); + } + + float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition); + //the closer the sub is, more likely it is to shoot frequently + float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, dist / 15000.0f); + 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 f203441b6..395621f48 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); @@ -113,13 +118,11 @@ 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; public readonly Location[] Locations; @@ -307,7 +310,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)) { @@ -378,7 +381,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); + var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); @@ -386,13 +389,20 @@ namespace Barotrauma #if CLIENT foreach (Character character in crewCharacters) { + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); character.Info?.GiveExperience(experienceGain, isMissionExperience: true); } #else foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) { //give the experience to the stored characterinfo if the client isn't currently controlling a character - (c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true); + CharacterInfo info = c.Character?.Info ?? c.CharacterInfo; + + var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); + info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); + + info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true); } #endif @@ -422,8 +432,7 @@ namespace Barotrauma { if (reputationReward.Key == "location") { - Locations[0].Reputation.AddReputation(reputationReward.Value); - Locations[1].Reputation.AddReputation(reputationReward.Value); + Locations[0].Reputation?.AddReputation(reputationReward.Value); } else { @@ -484,7 +493,7 @@ namespace Barotrauma 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++) @@ -504,7 +513,7 @@ namespace Barotrauma } else { - location.ChangeType(LocationType.Prefabs[change.ChangeToType]); + location.ChangeType(campaign, LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -518,7 +527,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; } @@ -527,7 +535,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; } @@ -619,4 +627,16 @@ namespace Barotrauma public Mission Mission { get; set; } } + class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier) + { + Value = missionExperienceGainMultiplier; + Mission = mission; + } + + public float Value { get; set; } + public Mission Mission { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 92f08baec..9bf5119a3 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,18 @@ namespace Barotrauma public readonly bool AllowRetry; + public readonly bool ShowInMenus, ShowStartMessage; + public readonly bool IsSideObjective; + public readonly bool AllowOtherMissionsInLevel; + public readonly bool RequireWreck, RequireRuin; + public readonly bool ShowProgressBar; + 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 +150,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 +173,24 @@ 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); 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); + 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 +364,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..dab6af924 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -5,40 +5,165 @@ 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; + + public enum RetrievalState { - if (item == null) + None = 0, + PickedUp = 1, + RetrievedToSub = 2 + } + + 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 => + RequiredRetrievalState == RetrievalState.RetrievedToSub ? + State == RetrievalState.RetrievedToSub : + State != RetrievalState.None; + + private RetrievalState state; + public RetrievalState State + { + get { return state; } + set { - Enumerable.Empty(); + if (value == state) { return; } + state = value; +#if SERVER + GameMain.Server?.UpdateMissionState(mission); +#endif + } + } + + 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 +171,227 @@ 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)); + 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; + } + 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: + var root = target.Item?.GetRootContainer() ?? target.Item; + if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1) { - return; + target.State = Target.RetrievalState.PickedUp; + if (target.Retrieved) { State = i + 1 ; } } - } - State = 1; - break; - case 1: - if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } - State = 2; - break; + break; + case Target.RetrievalState.PickedUp: + Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine; + if (parentSub != null && parentSub.Info.Type == SubmarineType.Player) + { + target.State = Target.RetrievalState.RetrievedToSub; + if (target.Retrieved) { State = i + 1; } + } + break; + } + } + 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.None); + 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/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 9ea8ff2a0..d33271bdf 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 { @@ -218,6 +224,8 @@ namespace Barotrauma.Extensions return new Dictionary(immutableDictionary); } + public static NetCollection ToNetCollection(this IEnumerable enumerable) => new NetCollection(enumerable.ToImmutableArray()); + /// /// Returns whether a given collection has at least a certain amount /// of elements for which the predicate returns true. diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 4d06bc9a9..4347b8687 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -163,6 +163,7 @@ namespace Barotrauma { if (!subs.Contains(item.Submarine)) { continue; } if (item.GetRootInventoryOwner() is Character) { continue; } + if (item.NonInteractable) { continue; } containers.AddRange(item.GetComponents()); } containers.Shuffle(Rand.RandSync.ServerAndClient); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 6fa21d9ae..edcf1532a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -158,6 +158,16 @@ namespace Barotrauma this.campaign = campaign; } + public static bool HasUnlockedStoreItem(ItemPrefab prefab) + { + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.HasStoreAccessForItem(prefab)) { return true; } + } + + return false; + } + private List GetItems(Identifier identifier, Dictionary> items, bool create = false) { if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index 06dc62593..a3e33ab98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -8,19 +8,14 @@ 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 CampaignMetadata(XElement element) { - Campaign = campaign; - foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 581566f00..f0fb1c47a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,9 +1,17 @@ #nullable enable using Microsoft.Xna.Framework; -using System; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { + public enum FactionAffiliation + { + Positive, + Neutral, + Negative + } + class Faction { public Reputation Reputation { get; } @@ -14,6 +22,42 @@ namespace Barotrauma Prefab = prefab; Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } + + /// + /// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents + /// + /// + public static FactionAffiliation GetPlayerAffiliationStatus(Identifier identifier, ImmutableHashSet? characterList = null) + { + if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; } + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + foreach (Character character in characterList) + { + if (character.Info is not { } info) { continue; } + + foreach (Faction faction in factions) + { + Identifier factionIdentifier = faction.Prefab.Identifier; + if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f) + { + return factionIdentifier == identifier + ? FactionAffiliation.Positive + : FactionAffiliation.Negative; + } + } + } + + return FactionAffiliation.Neutral; + } + + public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet? characterList = null) => GetPlayerAffiliationStatus(faction.Prefab.Identifier, characterList); + + public override string ToString() + { + return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})"; + } } internal class FactionPrefab : Prefab @@ -25,6 +69,50 @@ 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 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); + } + } + + public ImmutableArray AutomaticMissions; + + public bool StartOutpost { get; } + + public int MenuOrder { get; } /// @@ -42,38 +130,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 fe45446d3..fa72f23cb 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; } @@ -66,10 +65,21 @@ namespace Barotrauma float reputationGainMultiplier = 1f; foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { - reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier); + reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0; } reputationChange *= reputationGainMultiplier; } + else if (reputationChange < 0f) + { + float reputationLossMultiplier = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier); + reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0; + } + reputationChange *= reputationLossMultiplier; + } Value += reputationChange; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 1fa50625a..25fe21a47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -24,8 +24,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 @@ -45,7 +43,8 @@ namespace Barotrauma public UpgradeManager UpgradeManager; public MedicalClinic MedicalClinic; - public List Factions; + private List factions; + public IReadOnlyList Factions => factions; public CampaignMetadata CampaignMetadata; @@ -139,6 +138,15 @@ namespace Barotrauma public virtual bool PurchasedLostShuttles { get; set; } public virtual bool PurchasedItemRepairs { get; set; } + 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))); + } + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { @@ -211,7 +219,7 @@ namespace Barotrauma return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } - public List GetSubsToLeaveBehind(Submarine leavingSub) + public static List GetSubsToLeaveBehind(Submarine leavingSub) { //leave subs behind if they're not docked to the leaving sub and not at the same exit return Submarine.Loaded.FindAll(sub => @@ -266,7 +274,7 @@ namespace Barotrauma wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); } - public int GetHullRepairCost() + public static int GetHullRepairCost() { float totalDamage = 0; foreach (Structure wall in Structure.WallList) @@ -283,7 +291,7 @@ namespace Barotrauma return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost); } - public int GetItemRepairCost() + public static int GetItemRepairCost() { float totalRepairDuration = 0.0f; foreach (Item item in Item.ItemList) @@ -299,12 +307,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)); } } @@ -341,10 +349,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)); @@ -357,7 +364,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."); @@ -383,15 +390,95 @@ 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)) + { + if (currentLocation.Faction != faction && currentLocation.SecondaryFaction != faction && + map.SelectedLocation?.Faction != faction && map.SelectedLocation?.SecondaryFaction != faction) + { + continue; + } + foreach (var automaticMission in faction.Prefab.AutomaticMissions) + { + if (faction.Reputation.Value < automaticMission.MinReputation || faction.Reputation.Value > automaticMission.MaxReputation) { 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) @@ -486,13 +573,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; @@ -537,8 +617,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 { @@ -551,7 +655,7 @@ namespace Barotrauma /// /// Which submarine is at a position where it can leave the level and enter another one (if any). /// - private Submarine GetLeavingSub() + private static Submarine GetLeavingSub() { if (Level.IsLoadedOutpost) { @@ -606,8 +710,17 @@ namespace Barotrauma static Submarine GetLeavingSubAtEnd(IEnumerable leavingPlayers) { + if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any()) + { + Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + 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) { @@ -738,8 +851,9 @@ namespace Barotrauma foreach (Location location in Map.Locations) { location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty); - location.Reset(); + location.Reset(this); } + Map.ClearLocationHistory(); Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); if (Map.Radiation != null) @@ -771,11 +885,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"); @@ -946,11 +1105,28 @@ 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.HumanPrefab?.Faction != null && Factions.FirstOrDefault(f => f.Prefab.Identifier == npc.HumanPrefab.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 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 abstract void Save(XElement element); @@ -1025,7 +1201,7 @@ namespace Barotrauma } } - protected void LeaveUnconnectedSubs(Submarine leavingSub) + protected static void LeaveUnconnectedSubs(Submarine leavingSub) { if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 1d96fa2fa..791d852b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -17,6 +18,9 @@ namespace Barotrauma [Serialize("", IsPropertySaveable.Yes)] public string PresetName { get; set; } = string.Empty; + [Serialize(true, IsPropertySaveable.Yes)] + public bool TutorialEnabled { get; set; } + [Serialize(false, IsPropertySaveable.Yes), NetworkSerialize] public bool RadiationEnabled { get; set; } @@ -103,12 +107,9 @@ namespace Barotrauma private static int GetAddedMissionCount() { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (!characters.Any()) { return 0; } + return characters.Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 9e1e19aa6..c9eb3dabf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -105,9 +105,9 @@ namespace Barotrauma #endif } CampaignID = currentCampaignID; - CampaignMetadata = new CampaignMetadata(this); + CampaignMetadata = new CampaignMetadata(); UpgradeManager = new UpgradeManager(this); - InitCampaignData(); + InitFactions(); } public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings) @@ -133,13 +133,13 @@ namespace Barotrauma } partial void InitProjSpecific(); - + public static string GetCharacterDataSavePath(string savePath) { - return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); + return Path.Combine(Path.GetDirectoryName(savePath), Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml"); } - public string GetCharacterDataSavePath() + public static string GetCharacterDataSavePath() { return GetCharacterDataSavePath(GameMain.GameSession.SavePath); } @@ -190,11 +190,11 @@ 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); + CampaignMetadata = new CampaignMetadata(subElement); break; case "upgrademanager": case "pendingupgrades": @@ -237,10 +237,10 @@ namespace Barotrauma }; } - CampaignMetadata ??= new CampaignMetadata(this); + CampaignMetadata ??= new CampaignMetadata(); UpgradeManager ??= new UpgradeManager(this); - InitCampaignData(); + InitFactions(); #if SERVER characterData.Clear(); string characterDataPath = GetCharacterDataSavePath(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs index a9942470b..bb9438b28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/Tutorials/TutorialPrefab.cs @@ -29,6 +29,14 @@ namespace Barotrauma public readonly Sprite Banner; + public readonly EndMessageInfo EndMessage; + + public enum EndType { None, Continue, Restart } + + public readonly record struct EndMessageInfo( + EndType EndType, + Identifier NextTutorialIdentifier); + public TutorialPrefab(ContentFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) { Order = element.GetAttributeInt("order", int.MaxValue); @@ -59,6 +67,13 @@ namespace Barotrauma } EventIdentifier = element.GetChildElement("scriptedevent")?.GetAttributeIdentifier("identifier", "") ?? Identifier.Empty; + + if (element.GetChildElement("endmessage") is ContentXElement endMessageElement) + { + EndMessage = new EndMessageInfo( + EndType: endMessageElement.GetAttributeEnum("type", EndType.None), + NextTutorialIdentifier: endMessageElement.GetAttributeIdentifier("nexttutorial", Identifier.Empty)); + } } public CharacterInfo GetTutorialCharacterInfo() diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1b56b6f5c..1ba565c1e 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 { @@ -72,7 +73,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]; } @@ -83,7 +87,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]; } @@ -245,13 +252,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; } @@ -295,10 +333,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); @@ -391,6 +430,7 @@ namespace Barotrauma } } + GameMode!.AddExtraMissions(LevelData); foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation @@ -566,7 +606,6 @@ namespace Barotrauma if (GameMode != null && Submarine != null) { missions.Clear(); - GameMode.AddExtraMissions(LevelData); missions.AddRange(GameMode.Missions); GameMode.Start(); foreach (Mission mission in missions) @@ -582,6 +621,9 @@ namespace Barotrauma } } +#if CLIENT + ObjectiveManager.ResetObjectives(); +#endif EventManager?.StartRound(Level.Loaded); SteamAchievementManager.OnStartRound(); @@ -622,7 +664,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(); @@ -677,7 +728,7 @@ namespace Barotrauma else { Submarine.SetPosition(spawnPos - Vector2.UnitY * 100.0f); - Submarine.NeutralizeBallast(); + Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } } @@ -686,6 +737,7 @@ namespace Barotrauma Submarine.NeutralizeBallast(); Submarine.EnableMaintainPosition(); } + } else { @@ -756,7 +808,7 @@ namespace Barotrauma /// public static ImmutableHashSet GetSessionCrewCharacters(CharacterType type) { - if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet.Empty; } + if (GameMain.GameSession.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } IEnumerable players; IEnumerable bots; @@ -766,8 +818,8 @@ namespace Barotrauma players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer); #elif CLIENT - players = crewManager.GetCharacters().Where(c => c.IsPlayer); - bots = crewManager.GetCharacters().Where(c => c.IsBot); + players = crewManager.GetCharacters().Where(static c => c.IsPlayer); + bots = crewManager.GetCharacters().Where(static c => c.IsBot); #endif if (type.HasFlag(CharacterType.Bot)) { @@ -835,7 +887,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); @@ -847,6 +899,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); } TabMenu.OnRoundEnded(); GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); + ObjectiveManager.ResetUI(); #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 91b63855f..2b40e6304 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -9,7 +9,7 @@ using Barotrauma.Networking; namespace Barotrauma { - internal partial class MedicalClinic + internal sealed partial class MedicalClinic { public enum NetworkHeader { @@ -18,7 +18,8 @@ namespace Barotrauma ADD_PENDING, REMOVE_PENDING, CLEAR_PENDING, - HEAL_PENDING + HEAL_PENDING, + ADD_EVERYTHING_TO_PENDING } public enum AfflictionSeverity @@ -43,23 +44,10 @@ namespace Barotrauma } [NetworkSerialize] - public struct NetHealRequest : INetSerializableStruct - { - public HealRequestResult Result; - } + public readonly record struct NetHealRequest(HealRequestResult Result) : INetSerializableStruct; [NetworkSerialize] - public struct NetRemovedAffliction : INetSerializableStruct - { - public NetCrewMember CrewMember; - public NetAffliction Affliction; - } - - public struct NetPendingCrew : INetSerializableStruct - { - [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] - public NetCrewMember[] CrewMembers; - } + public readonly record struct NetRemovedAffliction(NetCrewMember CrewMember, NetAffliction Affliction) : INetSerializableStruct; public struct NetAffliction : INetSerializableStruct { @@ -69,42 +57,18 @@ namespace Barotrauma [NetworkSerialize] public ushort Strength; + [NetworkSerialize] + public int VitalityDecrease; + [NetworkSerialize] public ushort Price; - public AfflictionSeverity AfflictionSeverity + public void SetAffliction(Affliction affliction, CharacterHealth characterHealth) { - get - { - if (Prefab is null) { return AfflictionSeverity.Low; } - - float normalizedStrength = Strength / Prefab.MaxStrength; - - // lesser than 0.1 - if (normalizedStrength <= 0.1) - { - return AfflictionSeverity.Low; - } - - // between 0.1 and 0.5 - if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) - { - return AfflictionSeverity.Medium; - } - - // greater than 0.5 - return AfflictionSeverity.High; - } - } - - public Affliction Affliction - { - set - { - Identifier = value.Identifier; - Strength = (ushort)Math.Ceiling(value.Strength); - Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier); - } + Identifier = affliction.Identifier; + Strength = (ushort)Math.Ceiling(affliction.Strength); + Price = (ushort)(affliction.Prefab.BaseHealCost + Strength * affliction.Prefab.HealCostMultiplier); + VitalityDecrease = (int)affliction.GetVitalityDecrease(characterHealth); } private AfflictionPrefab? cachedPrefab; @@ -146,17 +110,23 @@ namespace Barotrauma } } - public struct NetCrewMember : INetSerializableStruct + public record struct NetCrewMember : INetSerializableStruct { [NetworkSerialize] public int CharacterInfoID; [NetworkSerialize] - public NetAffliction[] Afflictions; + public ImmutableArray Afflictions; - public CharacterInfo CharacterInfo + public NetCrewMember(CharacterInfo info) { - set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); + CharacterInfoID = info.GetIdentifierUsingOriginalName(); + Afflictions = ImmutableArray.Empty; + } + + public NetCrewMember(CharacterInfo info, ImmutableArray afflictions): this(info) + { + Afflictions = afflictions; } public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) @@ -194,11 +164,11 @@ namespace Barotrauma private static bool IsOutpostInCombat() { - if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } + if (Level.Loaded is not { Type: LevelData.LevelType.Outpost }) { return false; } - IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); + IEnumerable crew = GetCrewCharacters().Where(static c => c.Character != null).Select(static c => c.Character).ToImmutableHashSet(); - foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) + foreach (Character npc in Character.CharacterList.Where(static c => c.TeamID == CharacterTeamType.FriendlyNPC)) { bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy); if (isInCombatWithCrew) { return true; } @@ -238,6 +208,20 @@ namespace Barotrauma PendingHeals.Clear(); } + private void AddEverythingToPending() + { + foreach (CharacterInfo info in GetCrewCharacters()) + { + if (info.Character?.CharacterHealth is not { } health) { continue; } + + var afflictions = GetAllAfflictions(health); + + if (afflictions.Length is 0) { continue; } + + InsertPendingCrewMember(new NetCrewMember(info, afflictions)); + } + } + private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) { foreach (NetCrewMember listMember in PendingHeals.ToList()) @@ -255,7 +239,7 @@ namespace Barotrauma newAfflictions.Add(pendingAffliction); } - pendingMember.Afflictions = newAfflictions.ToArray(); + pendingMember.Afflictions = newAfflictions.ToImmutableArray(); } if (!pendingMember.Afflictions.Any()) { continue; } @@ -280,9 +264,9 @@ namespace Barotrauma static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); } - private NetAffliction[] GetAllAfflictions(CharacterHealth health) + private ImmutableArray GetAllAfflictions(CharacterHealth health) { - IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a)); + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(IsHealable); List afflictions = new List(); @@ -298,19 +282,20 @@ namespace Barotrauma } else { - newAffliction = new NetAffliction { Affliction = affliction }; + newAffliction = new NetAffliction(); + newAffliction.SetAffliction(affliction, health); newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price); } afflictions.Add(newAffliction); } - return afflictions.ToArray(); + return afflictions.ToImmutableArray(); static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength)); } - public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); + 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; @@ -325,7 +310,7 @@ namespace Barotrauma } #endif - return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); + return Character.CharacterList.Where(static c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(static c => c.Info).ToImmutableArray(); } #if DEBUG && CLIENT 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/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index a46d24b71..e4c16b822 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -207,7 +207,7 @@ namespace Barotrauma price = 0; } - if (Campaign.TryPurchase(client, price)) + if (force || Campaign.TryPurchase(client, price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a660bbbf3..6872f409c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -493,6 +493,10 @@ namespace Barotrauma base.PutItem(item, i, user, removeItem, createNetworkEvent); #if CLIENT CreateSlots(); + if (character == Character.Controlled) + { + HintManager.OnObtainedItem(character, item); + } #endif if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 40585fad2..8e31e3387 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -524,8 +524,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 aa26c2e1d..5999d3b94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,9 +4,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; #if CLIENT using Barotrauma.Lights; #endif @@ -16,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; @@ -92,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; } @@ -165,7 +170,7 @@ namespace Barotrauma.Items.Components set { isOpen = value; - OpenState = (isOpen) ? 1.0f : 0.0f; + OpenState = isOpen ? 1.0f : 0.0f; } } @@ -227,6 +232,7 @@ namespace Barotrauma.Items.Components } IsActive = true; + doorList.Add(this); } public override void OnItemLoaded() @@ -366,6 +372,8 @@ namespace Barotrauma.Items.Components return; } + + bool isClosing = false; if ((!IsStuck && !IsJammed) || !isOpen) { @@ -391,11 +399,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 @@ -439,6 +456,10 @@ namespace Barotrauma.Items.Components } PushCharactersAway(); } + if (OutsideSubmarineFixture != null && Body.Enabled) + { + OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith; + } #if CLIENT UpdateConvexHulls(); #endif @@ -459,6 +480,10 @@ namespace Barotrauma.Items.Components ce = ce.Next; } } + if (OutsideSubmarineFixture != null) + { + OutsideSubmarineFixture.CollidesWith = Category.None; + } linkedGap.Open = 1.0f; IsOpen = false; #if CLIENT @@ -534,6 +559,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; @@ -557,7 +612,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/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 8188d52c9..c6b403f9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -63,6 +64,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No)] + public float RaycastRange { get; set; } + [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { @@ -70,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] + public float Reload + { + get; + set; + } + [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { @@ -77,6 +88,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreUser + { + get; + set; + } + private readonly List nodes = new List(); public IEnumerable Nodes { @@ -91,6 +109,10 @@ namespace Barotrauma.Items.Components private readonly Attack attack; + private Character user; + + private float reloadTimer; + public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { @@ -125,6 +147,7 @@ namespace Barotrauma.Items.Components charging = true; timer = Duration; IsActive = true; + user = character; #if SERVER if (GameMain.Server != null) { item.CreateServerEvent(this); } #endif @@ -144,6 +167,11 @@ namespace Barotrauma.Items.Components if (timer <= 0.0f) { + if (reloadTimer > 0.0f) + { + reloadTimer -= deltaTime; + return; + } IsActive = false; return; } @@ -196,6 +224,7 @@ namespace Barotrauma.Items.Components private void Discharge() { + reloadTimer = Reload; ApplyStatusEffects(ActionType.OnUse, 1.0f); FindNodes(item.WorldPosition, Range); if (attack != null) @@ -203,7 +232,7 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); + character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); } } DischargeProjSpecific(); @@ -214,6 +243,18 @@ namespace Barotrauma.Items.Components public void FindNodes(Vector2 worldPosition, float range) { + if (RaycastRange > 0.0f) + { + float angle = 0.0f; + float dir = 1; + if (item.body != null) + { + angle += item.body.Rotation; + dir = item.body.Dir; + } + worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir; + } + //see which submarines are within range so we can skip structures that are in far-away subs List submarinesInRange = new List(); foreach (Submarine sub in Submarine.Loaded) @@ -222,7 +263,7 @@ namespace Barotrauma.Items.Components { submarinesInRange.Add(sub); } - else + else if (sub != null) { Rectangle subBorders = new Rectangle( sub.Borders.X - (int)range, sub.Borders.Y + (int)range, @@ -263,26 +304,41 @@ namespace Barotrauma.Items.Components entitiesInRange.Add(structure); } + + nodes.Clear(); + if (RaycastRange > 0.0f) + { + nodes.Add(new Node(item.WorldPosition, -1)); + int parentNodeIndex = 0; + AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex); + } + else + { + nodes.Add(new Node(worldPosition, -1)); + } + + float totalRange = RaycastRange + range; foreach (Character character in Character.CharacterList) { - if (!character.Enabled) continue; - if (OutdoorsOnly && character.Submarine != null) continue; - if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue; + if (!character.Enabled) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } + if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; } - if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls) + if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls || + (RaycastRange > 0.0f && MathUtils.LineToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition) < range * range * RangeMultiplierInWalls)) { entitiesInRange.Add(character); + charactersInRange.Add((character, nodes[0])); } } - nodes.Clear(); - nodes.Add(new Node(worldPosition, -1)); - FindNodes(entitiesInRange, worldPosition, 0, range); + FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range); //construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge) for (int i = 0; i < nodes.Count; i++) { - if (nodes[i].ParentIndex < 0) continue; + if (nodes[i].ParentIndex < 0) { continue; } Node parentNode = nodes[nodes[i].ParentIndex]; float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f); float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition); @@ -292,7 +348,7 @@ namespace Barotrauma.Items.Components private void FindNodes(List entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange) { - if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return; + if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; } //find the closest structure int closestIndex = -1; @@ -434,20 +490,21 @@ namespace Barotrauma.Items.Components for (int j = 0; j < entitiesInRange.Count; j++) { var otherEntity = entitiesInRange[j]; - if (!(otherEntity is Character character)) continue; - if (OutdoorsOnly && character.Submarine != null) continue; + if (otherEntity is not Character character) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } if (targetStructure.IsHorizontal) { - if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue; - if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue; - if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue; + if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) { continue; } + if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) { continue; } + if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; } } else { - if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue; - if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue; - if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue; + if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; } + if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) { continue; } + if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) { continue; } } float closestNodeDistSqr = float.MaxValue; int closestNodeIndex = -1; @@ -473,7 +530,10 @@ namespace Barotrauma.Items.Components AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); nodes.Add(new Node(targetPos, parentNodeIndex)); entitiesInRange.RemoveAt(closestIndex); - charactersInRange.Add((character, nodes[parentNodeIndex])); + if (!charactersInRange.Any(c => c.character == character)) + { + charactersInRange.Add((character, nodes[parentNodeIndex])); + } FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange); } } @@ -483,7 +543,7 @@ namespace Barotrauma.Items.Components Vector2 diff = targetPos - currPos; float dist = diff.Length(); Vector2 normal = new Vector2(-diff.Y, diff.X) / dist; - for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f)) + for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f)) { //0 at the edges, 1 at the center float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f; 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 1c5986b4d..3dde8c2bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components set { attachedByDefault = value; } } - [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos { @@ -143,7 +143,11 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -151,23 +155,50 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; +#if DEBUG [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] +#else + [Serialize("0.0,0.0", IsPropertySaveable.No)] +#endif public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif + public float SwingSpeed { get; set; } +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenHolding { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenAiming { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenUsing { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No)] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool DisableHeadRotation { get; set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] @@ -185,7 +216,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, @@ -386,10 +417,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); } @@ -650,11 +682,10 @@ namespace Barotrauma.Items.Components return false; } Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true); - Structure attachTarget = Structure.GetAttachTarget(attachPos); - + Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine; int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( - i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); + i => i.Submarine == attachSubmarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) { #if CLIENT @@ -812,10 +843,18 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (item.body == null || !item.body.Enabled) { return; } + + Character owner = picker ?? item.GetRootInventoryOwner() as Character; + + if (owner != null) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime, owner); + } + if (picker == null || !picker.HasEquippedItem(item)) { if (Pusher != null) { Pusher.Enabled = false; } - if (attachTargetCell == null) { IsActive = false; } + if (attachTargetCell == null && owner == null) { IsActive = false; } return; } @@ -824,23 +863,7 @@ namespace Barotrauma.Items.Components Drawable = true; } - Vector2 swing = Vector2.Zero; - if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) - { - swingState += deltaTime; - swingState %= 1.0f; - if (SwingWhenHolding || - (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || - (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) - { - swing = swingAmount * new Vector2( - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, - PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); - } - } - - ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - + UpdateSwingPos(deltaTime, out Vector2 swingPos); if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); @@ -853,7 +876,7 @@ namespace Barotrauma.Items.Components scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; - picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle); + picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle); if (!aim) { var rope = GetRope(); @@ -890,6 +913,24 @@ namespace Barotrauma.Items.Components } } + public void UpdateSwingPos(float deltaTime, out Vector2 swingPos) + { + swingPos = Vector2.Zero; + if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) + { + swingState += deltaTime; + swingState %= 1.0f; + if (SwingWhenHolding || + (SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) || + (SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Shoot))) + { + swingPos = swingAmount * new Vector2( + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f, + PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f); + } + } + } + public override void ReceiveSignal(Signal signal, Connection connection) { //do nothing 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 d0d0fc59a..b1254de84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -214,8 +214,9 @@ namespace Barotrauma.Items.Components bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; if (aim) { + UpdateSwingPos(deltaTime, out Vector2 swingPos); hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); if (ac.InWater) { ac.LockFlippingUntil = (float)Timing.TotalTime + Reload; @@ -392,36 +393,37 @@ namespace Barotrauma.Items.Components float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier); + Character user = User; Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; if (Attack != null) { - Attack.SetUser(User); + Attack.SetUser(user); Attack.DamageMultiplier = damageMultiplier; if (targetLimb != null) { if (targetLimb.character.Removed) { return; } targetLimb.character.LastDamageSource = item; - Attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); + Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f); } else if (targetCharacter != null) { if (targetCharacter.Removed) { return; } targetCharacter.LastDamageSource = item; - Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f); } else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure) { if (targetStructure.Removed) { return; } - Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); + Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f); } else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } - var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, @@ -435,7 +437,7 @@ namespace Barotrauma.Items.Components else if (target.UserData is Holdable holdable && holdable.CanPush) { if (holdable.Item.Removed) { return; } - Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f); + Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f); RestoreCollision(); hitting = false; User = null; @@ -448,29 +450,32 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User); - -#if SERVER - if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits + ActionType conditionalActionType = ActionType.OnSuccess; + if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user)) { - GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData( - success ? ActionType.OnUse : ActionType.OnFailure, - targetItemComponent: null, - targetCharacter, targetLimb)); - - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + conditionalActionType = ActionType.OnFailure; + } + if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null) + { + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb)); + #if SERVER + if (GameMain.Server != null) //TODO: Log structure hits + { + string logStr = picker?.LogName + " used " + item.Name; + if (item.ContainedItems != null && item.ContainedItems.Any()) + { + logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; + } + logStr += " on " + targetCharacter.LogName + "."; + Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); + } + #endif } -#endif - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? { - ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); + ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier); } if (DeleteOnUse) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index cadea84be..fa2cf6dbd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -23,6 +23,8 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; set; } + [Serialize(true, IsPropertySaveable.No, description: "If the item is held in RightHand or LeftHand, apply extra force there")] + public bool ApplyToHands { get; set; } #if CLIENT private string particles; [Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")] @@ -70,13 +72,16 @@ namespace Barotrauma.Items.Components character.AnimController.Collider.ApplyForce(propulsion); - if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) - { - character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); - } - if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) - { - character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + if (ApplyToHands) + { + if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) + { + character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion); + } + if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) + { + character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion); + } } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 998d7d7c1..932657436 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -32,6 +32,20 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")] + public float ReloadSkillRequirement + { + get; + set; + } + + [Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")] + public float ReloadNoSkill + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")] public bool HoldTrigger { @@ -39,7 +53,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; @@ -60,6 +74,23 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")] + public float LaunchImpulse + { + get; + set; + } + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + public float Penetration { get; private set; } + + [Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")] + public float WeaponDamageModifier + { + get; + private set; + } + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { @@ -67,6 +98,9 @@ namespace Barotrauma.Items.Components private set; } + private readonly IReadOnlySet suitableProjectiles; + + private enum ChargingState { Inactive, @@ -99,6 +133,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); } @@ -167,7 +206,15 @@ namespace Barotrauma.Items.Components if (currentChargeTime < MaxChargeTime) { return false; } IsActive = true; - ReloadTimer = reload / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); + float baseReloadTime = reload; + float weaponSkill = character.GetSkillLevel("weapons"); + if (ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement) + { + //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5 + float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1); + baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure); + } + ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f); currentChargeTime = 0f; if (character != null) @@ -218,9 +265,9 @@ namespace Barotrauma.Items.Components { lastProjectile?.Item.GetComponent()?.Snap(); } - float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); + float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier; projectile.Launcher = item; - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) { @@ -244,39 +291,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 e7658a80b..6806ae2ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -636,11 +636,14 @@ namespace Barotrauma.Items.Components float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - targetItem.WorldPosition, - levelResource.DeattachTimer / levelResource.DeattachDuration, - GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + if (targetItem.Prefab.ShowHealthBar) + { + Character.Controlled?.UpdateHUDProgressBar( + this, + targetItem.WorldPosition, + levelResource.DeattachTimer / levelResource.DeattachDuration, + GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + } #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; @@ -689,7 +692,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 df595ca57..edf91b18e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -1,17 +1,26 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { class Throwable : Holdable { - private float throwPos; - private bool throwing, throwDone; + enum ThrowState + { + None, + Initiated, + Throwing + } + + private const float ThrowAngleStart = -MathHelper.PiOver2, ThrowAngleEnd = MathHelper.PiOver2; + private float throwAngle = ThrowAngleStart; private bool midAir; + private ThrowState throwState; + + //continuous collision detection is used while the item is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -27,7 +36,6 @@ namespace Barotrauma.Items.Components public Throwable(Item item, ContentXElement element) : base(item, element) { - //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); if (aimPos == Vector2.Zero) { aimPos = new Vector2(0.6f, 0.1f); @@ -36,22 +44,21 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - return characterUsable || character == null; //We do the actual throwing in Aim because Use might be used by chems + //actual throwing logic is handled in Update + return characterUsable || character == null; } public override bool SecondaryUse(float deltaTime, Character character = null) { - if (!throwDone) return false; //This should only be triggered in update - throwDone = false; - return true; + //actual throwing logic is handled in Update - SecondaryUse only triggers when the item is thrown + return false; } public override void Drop(Character dropper) { base.Drop(dropper); - - throwing = false; - throwPos = 0.0f; + throwState = ThrowState.None; + throwAngle = ThrowAngleStart; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -100,13 +107,22 @@ namespace Barotrauma.Items.Components return; } - if (picker.IsKeyDown(InputType.Aim) && picker.IsKeyHit(InputType.Shoot)) { throwing = true; } - if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; } - bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; + if (throwState != ThrowState.Throwing) + { + if (picker.IsKeyDown(InputType.Aim)) + { + if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; } + } + else if (throwState != ThrowState.Initiated) + { + throwAngle = ThrowAngleStart; + } + } + bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim; if (picker.IsDead || !picker.AllowInput) { - throwing = false; + throwState = ThrowState.None; aim = false; } @@ -124,25 +140,29 @@ namespace Barotrauma.Items.Components item.Submarine = picker.Submarine; - if (!throwing) + if (throwState != ThrowState.Throwing) { - if (aim) + if (aim || throwState == ThrowState.Initiated) { - throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = System.Math.Min(throwAngle + deltaTime * 8.0f, ThrowAngleEnd); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); + if (throwAngle >= ThrowAngleEnd && throwState == ThrowState.Initiated) + { + throwState = ThrowState.Throwing; + } } else { - throwPos = 0; + throwAngle = ThrowAngleStart; ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { - throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); + throwAngle = MathUtils.WrapAnglePi(throwAngle - deltaTime * 15.0f); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwAngle); - if (throwPos < 0) + if (throwAngle < 0) { Vector2 throwVector = Vector2.Normalize(picker.CursorWorldPosition - picker.WorldPosition); //throw upwards if cursor is at the position of the character @@ -180,8 +200,7 @@ namespace Barotrauma.Items.Components Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; - throwPos = 0; - throwDone = true; + throwAngle = ThrowAngleStart; IsActive = true; if (GameMain.NetworkMember is { IsServer: true }) @@ -193,7 +212,7 @@ namespace Barotrauma.Items.Components //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); } - throwing = false; + throwState = ThrowState.None; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index af53b043f..61abd740f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -111,6 +111,13 @@ namespace Barotrauma.Items.Components private bool drawable = true; + [Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)] + public PropertyConditional.Comparison IsActiveConditionalComparison + { + get; + set; + } + public List IsActiveConditionals; public bool Drawable @@ -241,6 +248,11 @@ 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 => item.Speed; + public ItemComponent(Item item, ContentXElement element) { this.item = item; @@ -431,7 +443,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; } @@ -814,7 +826,7 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f) + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f) { if (statusEffectLists == null) { return; } @@ -828,11 +840,6 @@ namespace Barotrauma.Items.Components if (user != null) { effect.SetUser(user); } effect.AfflictionMultiplier = afflictionMultiplier; item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); - if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) - { - effect.AfflictionMultiplier = applyOnUserFraction; - item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); - } effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a1d69ce93..fac2fc0f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -65,8 +65,16 @@ namespace Barotrauma.Items.Components public int Capacity { get { return capacity; } - set { capacity = Math.Max(value, 0); } + private set + { + capacity = Math.Max(value, 0); + MainContainerCapacity = value; + } } + /// + /// The capacity of the main container without taking the sub containers into account. Only differs when there's a sub container defined for the component. + /// + public int MainContainerCapacity { get; private set; } //how many items can be contained private int maxStackSize; @@ -229,6 +237,9 @@ namespace Barotrauma.Items.Components public ImmutableHashSet ContainableItemIdentifiers => containableItemIdentifiers; public List ContainableItems { get; } + public List AllSubContainableItems { get; } + + public readonly bool HasSubContainers; public ItemContainer(Item item, ContentXElement element) : base(item, element) @@ -251,6 +262,7 @@ namespace Barotrauma.Items.Components break; case "subcontainer": totalCapacity += subElement.GetAttributeInt("capacity", 1); + HasSubContainers = true; break; } } @@ -270,7 +282,7 @@ namespace Barotrauma.Items.Components int subCapacity = subElement.GetAttributeInt("capacity", 1); int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); - List subContainableItems = null; + var subContainableItems = new List(); foreach (var subSubElement in subElement.Elements()) { if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } @@ -281,8 +293,9 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } - subContainableItems ??= new List(); subContainableItems.Add(containable); + AllSubContainableItems ??= new List(); + AllSubContainableItems.Add(containable); } for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) @@ -357,6 +370,14 @@ namespace Barotrauma.Items.Components //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); + + if (IsActive && item.GetRootInventoryOwner() is Character owner && + owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + // 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); + } + OnContainedItemsChanged.Invoke(this); } @@ -409,6 +430,20 @@ namespace Barotrauma.Items.Components return false; } + public override void FlipX(bool relativeToSub) + { + base.FlipX(relativeToSub); + if (HideItems) { return; } + if (item.body == null) { return; } + foreach (Item containedItem in Inventory.AllItems) + { + if (containedItem.body != null && containedItem.body.Enabled && containedItem.body.Dir != item.body.Dir) + { + containedItem.FlipX(relativeToSub); + } + } + } + public override void Update(float deltaTime, Camera cam) { if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) @@ -477,7 +512,7 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets)); + effect.AddNearbyTargets(item.WorldPosition, targets); effect.Apply(ActionType.OnActive, deltaTime, item, targets); } } @@ -582,11 +617,53 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { IsActive = true; + SetContainedActive(false); } public override void Equip(Character character) { IsActive = true; + if (character != null && character.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand))) + { + SetContainedActive(true); + } + } + + private void SetContainedActive(bool active) + { + foreach (Item containedItem in Inventory.AllItems) + { + RelatedItem containableItem = FindContainableItem(containedItem); + if (containableItem != null && containableItem.SetActive) + { + foreach (var ic in containedItem.Components) + { + ic.IsActive = active; + } + if (containedItem.body != null) + { + containedItem.body.Enabled = active; + if (active) + { + containedItem.body.PhysEnabled = false; + } + } + } + } + if (active) + { + FlipX(false); + } + } + + private RelatedItem FindContainableItem(Item item) + { + var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item)); + if (relatedItem == null && AllSubContainableItems != null) + { + relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item)); + } + return relatedItem; } public override void ReceiveSignal(Signal signal, Connection connection) @@ -604,6 +681,7 @@ namespace Barotrauma.Items.Components } } +#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods. public void SetContainedItemPositions() { Vector2 transformedItemPos = ItemPos * item.Scale; @@ -657,29 +735,70 @@ namespace Barotrauma.Items.Components transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); transformedItemPos += item.Position; } - } - - float currentRotation = itemRotation; - if (item.body != null) - { - currentRotation *= item.body.Dir; - currentRotation += item.body.Rotation; - } - else - { - currentRotation += -item.RotationRad; } int i = 0; Vector2 currentItemPos = transformedItemPos; foreach (Item contained in Inventory.AllItems) { + Vector2 itemPos = currentItemPos; + var relatedItem = FindContainableItem(contained); + if (relatedItem != null) + { + if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; } + if (relatedItem.ItemPos.HasValue) + { + 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) + { + 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; + } + } + } + } + if (contained.body != null) { try { - Vector2 simPos = ConvertUnits.ToSimUnits(currentItemPos); - contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, currentRotation); + Vector2 simPos = ConvertUnits.ToSimUnits(itemPos); + float rotation = itemRotation; + if (relatedItem != null && relatedItem.Rotation != 0) + { + rotation = MathHelper.ToRadians(relatedItem.Rotation); + } + if (item.body != null) + { + rotation *= item.body.Dir; + rotation += item.body.Rotation; + } + else + { + rotation += -item.RotationRad; + } + contained.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation); contained.body.SetPrevTransform(contained.body.SimPosition, contained.body.Rotation); contained.body.UpdateDrawPosition(); } @@ -695,8 +814,8 @@ namespace Barotrauma.Items.Components contained.Rect = new Rectangle( - (int)(currentItemPos.X - contained.Rect.Width / 2.0f), - (int)(currentItemPos.Y + contained.Rect.Height / 2.0f), + (int)(itemPos.X - contained.Rect.Width / 2.0f), + (int)(itemPos.Y + contained.Rect.Height / 2.0f), contained.Rect.Width, contained.Rect.Height); contained.Submarine = item.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 7f307471f..f8df97352 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -104,12 +104,14 @@ namespace Barotrauma.Items.Components // doesn't quite work properly, remaining time changes if tinkering stops float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); + float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); + if (DeconstructItemsSimultaneously) { float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier); + deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -139,7 +141,7 @@ namespace Barotrauma.Items.Components if (targetItem == null) { return; } var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -218,7 +220,7 @@ namespace Barotrauma.Items.Components if (percentageHealth < deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } - if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(deconstructProduct.ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); return; @@ -457,6 +459,12 @@ namespace Barotrauma.Items.Components progressTimer = 0.0f; progressState = 0.0f; } +#if CLIENT + else + { + HintManager.OnStartDeconstructing(user, this); + } +#endif inputContainer.Inventory.Locked = IsActive; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 13f858f37..d6ae48ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -30,11 +30,8 @@ namespace Barotrauma.Items.Components Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { - get { return maxForce; } - set - { - maxForce = Math.Max(0.0f, value); - } + get => maxForce; + set => maxForce = Math.Max(0.0f, value); } [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, @@ -94,7 +91,7 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { UpdateOnActiveEffects(deltaTime); @@ -129,12 +126,14 @@ namespace Barotrauma.Items.Components { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - currForce *= maxForce * forceMultiplier; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; + if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } + currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce); + //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 9047d2f5e..e2c8cd0b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; - } + } } var fabricationRecipes = new Dictionary(); @@ -104,6 +104,18 @@ namespace Barotrauma.Items.Components continue; } } + + bool recipeInvalid = false; + foreach (var requiredItem in recipe.RequiredItems) + { + if (requiredItem.ItemPrefabs.None()) + { + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\"."); + recipeInvalid = true; + } + } + if (recipeInvalid) { continue; } + fabricationRecipes.Add(recipe.RecipeHash, recipe); if (recipe.FabricationLimitMax >= 0) { @@ -356,9 +368,10 @@ namespace Barotrauma.Items.Components bool ingredientsStolen = false; bool ingredientsAllowStealing = true; - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { - fabricatedItem.RequiredItems.ForEach(requiredItem => + List foundAvailableItems = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) { for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { @@ -367,10 +380,7 @@ namespace Barotrauma.Items.Components if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availableItems = availableIngredients[requiredPrefab.Identifier]; - var availableItem = availableItems.FirstOrDefault(potentialPrefab => - { - return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); - }); + var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage)); if (availableItem == null) { continue; } @@ -401,13 +411,21 @@ namespace Barotrauma.Items.Components } } + foundAvailableItems.Add(availableItem); availableItems.Remove(availableItem); - Entity.Spawner.AddItemToRemoveQueue(availableItem); - inputContainer.Inventory.RemoveItem(availableItem); break; } } - }); + } + + var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); + user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); + + foreach (Item availableItem in fabricationIngredients.Items) + { + Entity.Spawner.AddItemToRemoveQueue(availableItem); + inputContainer.Inventory.RemoveItem(availableItem); + } int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); @@ -535,12 +553,13 @@ namespace Barotrauma.Items.Components return currPowerConsumption; } - private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { - if (user == null) { return 0; } + if (user?.Info == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; + floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false); foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); @@ -637,9 +656,14 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster - return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); + float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); + if (user is not null && fabricableItem.TargetItem is { } it && it.Tags.Contains("medical")) + { + time *= 1f + user.GetStatValue(StatTypes.FabricateMedicineSpeedMultiplier); + } + return time; } - + public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { if (skills.Length == 0) { return 1.0f; } @@ -713,7 +737,31 @@ namespace Barotrauma.Items.Components { availableIngredients[itemIdentifier] = new List(itemList.Count); } - availableIngredients[itemIdentifier].Add(item); + //order by condition (prefer using worst-condition items) + int index = 0; + while (index < availableIngredients[itemIdentifier].Count && + compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0) + { + index++; + } + + static int compare(Item item1, Item item2, Inventory inputInventory) + { + bool item1InInputInventory = item1.ParentInventory == inputInventory; + bool item2InInputInventory = item2.ParentInventory == inputInventory; + //prefer items in the input inventory + if (item1InInputInventory != item2InInputInventory) + { + return item1InInputInventory ? 1 : -1; + } + else + { + //prefer items in worse condition + return Math.Sign(item2.Condition - item1.Condition); + } + } + + availableIngredients[itemIdentifier].Insert(index, item); } } @@ -827,5 +875,15 @@ namespace Barotrauma.Items.Components public float Value { get; set; } public ItemPrefab ItemPrefab { get; set; } } + + internal sealed class AbilityFabricationItemIngredients : AbilityObject + { + public List Items { get; set; } + + public AbilityFabricationItemIngredients(List items) + { + Items = items; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index a3588ac5c..8a3cd4d15 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components public float? ReceivedOxygenAmount, ReceivedWaterAmount; + public double LastOxygenDataTime, LastWaterDataTime; + public readonly HashSet Cards = new HashSet(); public bool Distort; @@ -83,7 +85,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - //periodically reset all hull data + //reset data if we haven't received anything in a while //(so that outdated hull info won't be shown if detectors stop sending signals) if (DateTime.Now > resetDataTime) { @@ -91,8 +93,8 @@ namespace Barotrauma.Items.Components { if (!hullData.Distort) { - hullData.ReceivedOxygenAmount = null; - hullData.ReceivedWaterAmount = null; + if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; } + if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; } } } resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1); @@ -159,6 +161,7 @@ namespace Barotrauma.Items.Components //cheating a bit because water detectors don't actually send the water level bool fromWaterDetector = source.GetComponent() != null; hullData.ReceivedWaterAmount = null; + hullData.LastWaterDataTime = Timing.TotalTime; if (fromWaterDetector) { hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull); @@ -184,9 +187,10 @@ namespace Barotrauma.Items.Components oxy = Rand.Range(0.0f, 100.0f); } hullData.ReceivedOxygenAmount = oxy; + hullData.LastOxygenDataTime = Timing.TotalTime; foreach (var linked in sourceHull.linkedTo) { - if (!(linked is Hull linkedHull)) { continue; } + if (linked is not Hull linkedHull) { continue; } if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData)) { linkedHullData = new HullData(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index db2eae084..0401bda5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -57,8 +57,8 @@ namespace Barotrauma.Items.Components [Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { - get { return maxFlow; } - set { maxFlow = value; } + get => maxFlow; + set => maxFlow = value; } [Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] @@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { pumpSpeedLockTimer -= deltaTime; isActiveLockTimer -= deltaTime; - if (!IsActive) { return; } + if (!IsActive) + { + return; + } currFlow = 0.0f; @@ -122,7 +125,10 @@ namespace Barotrauma.Items.Components FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } - if (!HasPower) { return; } + if (!HasPower) + { + return; + } UpdateProjSpecific(deltaTime); @@ -132,13 +138,15 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); - currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; + currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + if (item.GetComponent() is { IsTinkering: true } repairable) { currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } + currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow); + //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); @@ -227,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 7fc66baf2..9a46630cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -95,15 +95,12 @@ namespace Barotrauma.Items.Components } } } - + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { - get { return maxPowerOutput; } - set - { - maxPowerOutput = Math.Max(0.0f, value); - } + get => maxPowerOutput; + set => maxPowerOutput = Math.Max(0.0f, value); } [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")] @@ -152,11 +149,11 @@ namespace Barotrauma.Items.Components turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { - get { return fuelConsumptionRate; } + get => fuelConsumptionRate; set { if (!MathUtils.IsValid(value)) return; @@ -256,6 +253,8 @@ namespace Barotrauma.Items.Components } #endif + float maxPowerOut = GetMaxOutput(); + if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1) { TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); @@ -289,9 +288,9 @@ namespace Barotrauma.Items.Components //use a smoothed "correct output" instead of the actual correct output based on the load //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily - if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) + if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f)) { - CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user @@ -350,7 +349,7 @@ namespace Barotrauma.Items.Components if (!isConnectedToFriendlyOutpost) { - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime; } } fuelLeft += item.ConditionPercentage; @@ -359,10 +358,10 @@ namespace Barotrauma.Items.Components if (fissionRate > 0.0f) { - if (item.AiTarget != null && MaxPowerOutput > 0) + if (item.AiTarget != null && maxPowerOut > 0) { var aiTarget = item.AiTarget; - float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; + float range = Math.Abs(currPowerConsumption) / maxPowerOut; aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); if (item.CurrentHull != null) { @@ -433,15 +432,17 @@ namespace Barotrauma.Items.Components tolerance = 3f; } + float maxPowerOut = GetMaxOutput(); + float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); - float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); - float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); + float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); + float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); minUpdatePowerOut = minOutput; maxUpdatePowerOut = maxOutput; - float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut; - + float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut; + return new PowerRange(minOutput, maxOutput, reactorMax); } @@ -464,11 +465,13 @@ namespace Barotrauma.Items.Components float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut); float newLoad = loadLeft; + float maxOutput = GetMaxOutput(); + //Adjust behaviour for multi reactor setup - if (MaxPowerOutput != minMaxPower.ReactorMaxOutput) + if (maxOutput != minMaxPower.ReactorMaxOutput) { - float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; - float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); + float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft; + float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100); newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft); } @@ -509,7 +512,7 @@ namespace Barotrauma.Items.Components //calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate); float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f); - float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput; + float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput(); //maximum output not enough, we need more fuel return theoreticalMaxOutput < Load * minimumOutputRatio; @@ -670,7 +673,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(); @@ -693,7 +696,7 @@ namespace Barotrauma.Items.Components aiUpdateTimer = AIUpdateInterval; // load more fuel if the current maximum output is only 50% of the current load // or if the fuel rod is (almost) deplenished - float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); + float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) { bool outOfFuel = false; @@ -871,5 +874,8 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; } } } + + private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 077ae053f..26f34b806 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -153,13 +153,6 @@ namespace Barotrauma.Items.Components bool changed = currentMode != value; currentMode = value; - if (value == Mode.Passive) - { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } - } #if CLIENT if (changed) { prevPassivePingRadius = float.MaxValue; } UpdateGUIElements(); @@ -206,13 +199,6 @@ namespace Barotrauma.Items.Components var activePing = activePings[currentPingIndex]; if (activePing.State > 1.0f) { - if (item.AiTarget != null) - { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); - item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); - item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f; - item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); - } aiPingCheckPending = true; currentPingIndex = -1; } @@ -228,21 +214,27 @@ namespace Barotrauma.Items.Components activePings[currentPingIndex].Direction = pingDirection; activePings[currentPingIndex].State = 0.0f; activePings[currentPingIndex].PrevPingRadius = 0.0f; + if (item.AiTarget != null) + { + item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; + item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); + } item.Use(deltaTime); } } else { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } aiPingCheckPending = false; } } for (var pingIndex = 0; pingIndex < activePingsCount;) { + if (item.AiTarget != null) + { + float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePings[pingIndex].State / zoom); + item.AiTarget.SoundRange = Math.Max(item.AiTarget.SoundRange, MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range)); + } if (activePings[pingIndex].State > 1.0f) { var lastIndex = --activePingsCount; @@ -281,7 +273,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 75776f814..cc73b26fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -144,6 +144,11 @@ namespace Barotrauma.Items.Components } } + public float TargetVelocityLengthSquared + { + get => TargetVelocity.LengthSquared(); + } + public Vector2 SteeringInput { get { return steeringInput; } @@ -715,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) @@ -808,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 67c2f4a94..4deab6360 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { - get { return capacity; } + get => capacity; set { capacity = Math.Max(value, 1.0f); } } @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components } } - public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); + public float ChargePercentage => MathUtils.Percentage(Charge, GetCapacity()); [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed @@ -125,10 +125,19 @@ namespace Barotrauma.Items.Components set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } } + private bool flipIndicator; + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")] + public bool FlipIndicator + { + get { return flipIndicator; } + set { flipIndicator = value; } + } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; private bool isRunning; + public bool HasBeenTuned { get; private set; } public PowerContainer(Item item, ContentXElement element) @@ -146,7 +155,7 @@ namespace Barotrauma.Items.Components return picker != null; } - public override void Update(float deltaTime, Camera cam) + public override void Update(float deltaTime, Camera cam) { if (item.Connections == null) { @@ -283,12 +292,12 @@ namespace Barotrauma.Items.Components else { //Decrease charge based on how much power is leaving the device - Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, GetCapacity()); prevCharge = Charge; } } - 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; } @@ -370,5 +379,7 @@ namespace Barotrauma.Items.Components } } } + + public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index e355ccb14..88db0df19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components { if (!powerOnSoundPlayed && powerOnSound != null) { - SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling); + SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier()); powerOnSoundPlayed = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b2d26005a..8bf71c17b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -279,13 +279,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); @@ -387,11 +387,12 @@ 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); + DoLaunch(launchDir * modifiedLaunchImpulse); System.Diagnostics.Debug.WriteLine("launch: " + modifiedLaunchImpulse + " - " + item.body.LinearVelocity); } } User = character; + ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User); return true; } @@ -412,18 +413,29 @@ namespace Barotrauma.Items.Components 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; - item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; + 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; } - IsActive = true; if (stickJoint == null) { return; } @@ -552,6 +564,7 @@ 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 is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; } @@ -592,6 +605,7 @@ namespace Barotrauma.Items.Components if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; } + if (fixture.CollidesWith == Category.None) { return -1; } if (!(fixture.Body.UserData is Holdable holdable && holdable.CanPush)) { //ignore everything else than characters, sub walls and level walls @@ -731,11 +745,13 @@ 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) { return false; } + if (target.IsSensor) { return false; } if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine) { @@ -772,7 +788,7 @@ namespace Barotrauma.Items.Components { item.body.FarseerBody.ResetDynamics(); } - if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) + if (hits.Count >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { DisableProjectileCollisions(); return true; @@ -803,6 +819,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); @@ -849,7 +872,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; @@ -874,14 +897,14 @@ 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; } if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent() != null) && targetItem.Condition > 0) { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, @@ -915,23 +938,22 @@ namespace Barotrauma.Items.Components if (character != null) { character.LastDamageSource = item; } - ActionType actionType = ActionType.OnUse; - if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user)) + ActionType conditionalActionType = ActionType.OnSuccess; + if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User)) { - actionType = ActionType.OnFailure; + conditionalActionType = ActionType.OnFailure; } - #if CLIENT - PlaySound(actionType, user: _user); - PlaySound(ActionType.OnImpact, user: _user); + PlaySound(conditionalActionType, user: User); + PlaySound(ActionType.OnImpact, user: User); #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user); + ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User); var attack = targetLimb.attack; if (attack != null) { @@ -940,8 +962,6 @@ namespace Barotrauma.Items.Components { if (effect.type == ActionType.OnImpact) { - //effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); - if (effect.HasTargetType(StatusEffect.TargetType.This)) { effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); @@ -950,32 +970,27 @@ namespace Barotrauma.Items.Components effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets)); + effect.AddNearbyTargets(targetLimb.WorldPosition, targets); effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); } - } } } -#if SERVER - if (GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); } -#endif } else { - ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); -#if SERVER - if (GameMain.NetworkMember.IsServer) + ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User); + if (GameMain.NetworkMember is { IsServer: true } server) { - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); - GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); } -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e486300c6..e5604f449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -420,7 +421,8 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { - item.Condition -= DeteriorationSpeed * deltaTime; + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; } } return; @@ -467,8 +469,14 @@ namespace Barotrauma.Items.Components wasGoodCondition = true; } + float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed); + if (requiredSkills.Any(static skill => skill.Identifier == "mechanical")) + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + } + float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); - fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; + fixDuration /= 1 + talentMultiplier + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); @@ -500,7 +508,7 @@ namespace Barotrauma.Items.Components SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); - CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); + CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); } if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); @@ -687,4 +695,14 @@ namespace Barotrauma.Items.Components //where set_active/set_state signals can disable the component } } + + internal sealed class AbilityRepairable : AbilityObject, IAbilityItem + { + public Item Item { get; set; } + + public AbilityRepairable(Item item) + { + Item = item; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index dc2ae516c..56c6e2e11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -326,7 +326,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 32f151379..1ccd035f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components #if CLIENT Light.ParentSub = item.Submarine; #endif - if (item.Container != null) + if (item.Container != null && !(item.GetRootInventoryOwner() is Character)) { SetLightSourceState(false, 0.0f); return; @@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components if (body != null && !body.Enabled) { SetLightSourceState(false, 0.0f); - return; + return; } //currPowerConsumption = powerConsumption; 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 e60825beb..9e0fcb5e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -74,7 +74,13 @@ namespace Barotrauma.Items.Components return 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(); @@ -131,7 +137,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() @@ -144,7 +150,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; } @@ -169,6 +175,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) @@ -208,6 +223,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 df767e009..123f1b281 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -317,6 +317,39 @@ 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?"), Editable] + public float RandomAimAmount { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable] + public float RandomAimMinTime { get; private set; } + + [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly?"), Editable] + public float RandomAimMaxTime { get; private 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 always aim at targets without delay?"), Editable] + public bool IgnoreAimDelay { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters?"), Editable] + public bool TargetCharacters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target monsters?"), Editable] + public bool TargetMonsters { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target humans (or 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("", 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) { @@ -558,6 +591,11 @@ namespace Barotrauma.Items.Components } UpdateLightComponents(); + + if (AutoOperate) + { + UpdateAutoOperate(deltaTime); + } } public void UpdateLightComponents() @@ -656,13 +694,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) @@ -898,12 +943,20 @@ namespace Barotrauma.Items.Components 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; + bool targetCharacters = TargetCharacters || TargetHumans || TargetMonsters; + bool targetHumans = TargetCharacters && TargetHumans; + bool targetMonsters = TargetCharacters && TargetMonsters; + + if (friendlyTag.IsEmpty) + { + friendlyTag = FriendlyTag; + } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; @@ -922,7 +975,7 @@ namespace Barotrauma.Items.Components updateTimer -= deltaTime; } - if (!ignoreDelay && waitTimer > 0) + if (!IgnoreAimDelay && waitTimer > 0) { waitTimer -= deltaTime; return; @@ -932,12 +985,12 @@ 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; } + if (!friendlyTag.IsEmpty && (character.SpeciesName.Equals(friendlyTag) || character.Group.Equals(friendlyTag))) { continue; } bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; if (isHuman) { @@ -947,7 +1000,7 @@ namespace Barotrauma.Items.Components continue; } } - else if (!targetOtherCreatures) + else if (!targetMonsters) { // Don't target other creatures if not defined to. continue; @@ -958,7 +1011,7 @@ namespace Barotrauma.Items.Components closestDist = dist; } } - if (targetSubmarines) + if (TargetSubmarines) { if (target == null || target.Submarine != null) { @@ -966,6 +1019,7 @@ namespace Barotrauma.Items.Components foreach (Submarine sub in Submarine.Loaded) { if (sub.Info.Type != SubmarineType.Player) { continue; } + if (sub == Item.Submarine) { continue; } float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } closestSub = sub; @@ -985,28 +1039,33 @@ namespace Barotrauma.Items.Components } } } - 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 (!IgnoreAimDelay) + { + 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 (disorderTimer < 0) + { + // Random disorder + disorderTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime); + waitTimer = Rand.Range(0.25f, 1f); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-RandomAimAmount, RandomAimAmount)); + updatePending = true; + return; + } + else + { + disorderTimer -= deltaTime; + } } } if (target == null) { return; } @@ -1041,11 +1100,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) { @@ -1053,7 +1112,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) { @@ -1438,7 +1497,7 @@ namespace Barotrauma.Items.Components return 0; } - private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) + private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true) { if (targetBody == null) { return false; } Character targetCharacter = null; @@ -1459,9 +1518,9 @@ namespace Barotrauma.Items.Components return false; } } - if (ai != null) + if (!friendlyTag.IsEmpty) { - if (targetCharacter.Params.Group == ai.Config.Entity) + if (targetCharacter.SpeciesName.Equals(friendlyTag) || targetCharacter.Group.Equals(friendlyTag)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 57b4dc685..e61f4cfa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -270,6 +270,9 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + public bool AllowUseWhenWorn { get; set; } + public readonly int Variants; private int variant; @@ -512,8 +515,11 @@ namespace Barotrauma.Items.Components 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 afb94878c..2a9d2ff17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -226,6 +226,14 @@ namespace Barotrauma { foreach (var item in slots[i].Items) { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); +#endif + continue; + } + bool duplicateFound = false; for (int j = 0; j < i; j++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8181756f4..462058cca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -138,7 +138,7 @@ namespace Barotrauma private ConcurrentQueue impactQueue; //a dictionary containing lists of the status effects in all the components of the item - private readonly bool[] hasStatusEffectsOfType; + private readonly bool[] hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; private readonly Dictionary> statusEffectLists; public Dictionary SerializableProperties { get; protected set; } @@ -422,8 +422,41 @@ namespace Barotrauma } } - public Color? HighlightColor; + /// + /// Can be used by status effects or conditionals to check whether the item is contained inside something + /// + public bool IsContained + { + get + { + return parentInventory != null; + } + } + /// + /// Can be used by status effects or conditionals to the speed of the item + /// + public float Speed + { + get + { + if (body != null && body.PhysEnabled) + { + return body.LinearVelocity.Length(); + } + else if (ParentInventory?.Owner is Character character) + { + return character.AnimController.MainLimb.LinearVelocity.Length(); + } + else if (container != null) + { + return container.Speed; + } + return 0.0f; + } + } + + public Color? HighlightColor; [Serialize("", IsPropertySaveable.Yes)] @@ -437,7 +470,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); } } } @@ -597,7 +631,7 @@ namespace Barotrauma { if (!spawnedInCurrentOutpost && value) { - OriginalOutpost = GameMain.GameSession?.StartLocation?.BaseName ?? ""; + OriginalOutpost = GameMain.GameSession?.LevelData?.Seed; } spawnedInCurrentOutpost = value; } @@ -618,7 +652,9 @@ namespace Barotrauma set { originalOutpost = value; - if (!string.IsNullOrEmpty(value) && GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && GameMain.GameSession?.StartLocation?.BaseName == value) + if (!string.IsNullOrEmpty(value) && + GameMain.GameSession?.LevelData?.Type == LevelData.LevelType.Outpost && + GameMain.GameSession?.LevelData?.Seed == value) { spawnedInCurrentOutpost = true; } @@ -821,6 +857,16 @@ namespace Barotrauma public bool IsSecondaryItem { get; } + private ItemStatManager statManager; + public ItemStatManager StatManager + { + get + { + statManager ??= new ItemStatManager(this); + return statManager; + } + } + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -900,7 +946,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 { @@ -962,7 +1008,6 @@ namespace Barotrauma } } - hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; foreach (ItemComponent ic in components) { if (ic is Pickable pickable) @@ -1611,7 +1656,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems)) { - targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets)); + effect.AddNearbyTargets(WorldPosition, targets); if (targets.Count > 0) { hasTargets = true; @@ -1656,8 +1701,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); } @@ -1837,16 +1889,32 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - bool shouldBeActive = true; - foreach (var conditional in ic.IsActiveConditionals) + if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) { - if (!ConditionalMatches(conditional)) + bool shouldBeActive = true; + foreach (var conditional in ic.IsActiveConditionals) { - shouldBeActive = false; - break; + if (!ConditionalMatches(conditional)) + { + shouldBeActive = false; + break; + } } + ic.IsActive = shouldBeActive; + } + else + { + bool shouldBeActive = false; + foreach (var conditional in ic.IsActiveConditionals) + { + if (ConditionalMatches(conditional)) + { + shouldBeActive = true; + break; + } + } + ic.IsActive = shouldBeActive; } - ic.IsActive = shouldBeActive; } #if CLIENT if (ic.HasSounds) @@ -1892,7 +1960,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); @@ -2072,7 +2143,7 @@ namespace Barotrauma } //no need to apply buoyancy if the item is still and not light enough to float - if (moving || body.Density < 10.0f) + if (moving || body.Density <= 10.0f) { Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity; body.ApplyForce(buoyancy); @@ -2699,38 +2770,26 @@ namespace Barotrauma } #endif - float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f; - bool remove = false; foreach (ItemComponent ic in components) { if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; } bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user); - ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; + ActionType conditionalActionType = success ? ActionType.OnSuccess : ActionType.OnFailure; #if CLIENT - ic.PlaySound(actionType, user); + ic.PlaySound(conditionalActionType, user); + ic.PlaySound(ActionType.OnUse, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction); - if (applyOnSelfFraction > 0.0f) - { - //hacky af - ic.statusEffectLists.TryGetValue(actionType, out var effectList); - if (effectList != null) - { - effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction); - ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user); - effectList.ForEach(e => e.AfflictionMultiplier = 1.0f); - } - } + ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData( - actionType, ic, character, targetLimb)); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } @@ -2866,15 +2925,20 @@ namespace Barotrauma //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); SerializableProperty property = extraData.SerializableProperty; + ISerializableEntity entity = extraData.Entity; if (property != null) { - var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.WriteByte((byte)allProperties.FindIndex(p => p.property == property)); + int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity); + if (propertyIndex < -1) + { + throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\""); + } + msg.WriteVariableUInt32((uint)propertyIndex); } - object value = property.GetValue(propertyOwner.obj); + object value = property.GetValue(entity); if (value is string stringVal) { msg.WriteString(stringVal); @@ -2979,7 +3043,7 @@ namespace Barotrauma int propertyIndex = 0; if (allProperties.Count > 1) { - propertyIndex = msg.ReadByte(); + propertyIndex = (int)msg.ReadVariableUInt32(); } bool allowEditing = true; @@ -3119,14 +3183,14 @@ namespace Barotrauma } logPropertyChangeCoroutine = CoroutineManager.Invoke(() => { - GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); + GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); }, delay: 1.0f); } #endif - if (GameMain.NetworkMember is { IsServer: true }) + if (GameMain.NetworkMember is { IsServer: true } && parentObject is ISerializableEntity entity) { - GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property, entity)); } } @@ -3230,7 +3294,7 @@ namespace Barotrauma { if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) { - GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property, item)); } } } @@ -3349,12 +3413,6 @@ namespace Barotrauma item.PurchasedNewSwap = false; } - item.condition = element.GetAttributeFloat("condition", item.condition); - item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); - item.lastSentCondition = item.condition; - item.RecalculateConditionValues(); - item.SetActiveSprite(); - Version savedVersion = submarine?.Info.GameVersion; if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase)) { @@ -3362,14 +3420,38 @@ namespace Barotrauma //(the sub may have already been saved and up-to-date, even though the character inventories aren't) savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0")); } - + + float prevCondition = item.condition; if (savedVersion != null) { SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion); } + if (element.GetAttribute("conditionpercentage") != null) + { + item.condition = element.GetAttributeFloat("conditionpercentage", 100.0f) / 100.0f * item.MaxCondition; + } + else + { + //backwards compatibility + item.condition = element.GetAttributeFloat("condition", item.condition); + //if the item was in full condition considering the unmodified health + //(not taking possible HealthMultipliers added by mods into account), + //make sure it stays in full condition + bool wasFullCondition = prevCondition >= item.Prefab.Health; + if (wasFullCondition) + { + item.condition = item.MaxCondition; + } + item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); + } + item.lastSentCondition = item.condition; + item.RecalculateConditionValues(); + item.SetActiveSprite(); + foreach (ItemComponent component in item.components) { + if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } @@ -3401,11 +3483,6 @@ namespace Barotrauma element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier)))); } - if (condition < MaxCondition) - { - element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture))); - } - if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f)) { element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture))); @@ -3442,6 +3519,16 @@ namespace Barotrauma upgrade.Save(element); } + if (condition < MaxCondition) + { + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); + } + else + { + var conditionAttribute = element.GetAttribute("condition"); + if (conditionAttribute != null) { conditionAttribute.Remove(); } + } + parentElement.Add(element); return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index e0c65a747..77b03826f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -18,9 +19,10 @@ namespace Barotrauma AssignCampaignInteraction = 6, ApplyStatusEffect = 7, Upgrade = 8, + ItemStat = 9, MinValue = 0, - MaxValue = 6 + MaxValue = 9 } public interface IEventData : NetEntityEvent.IData @@ -56,10 +58,24 @@ namespace Barotrauma { public EventType EventType => EventType.ChangeProperty; public readonly SerializableProperty SerializableProperty; + public readonly ISerializableEntity Entity; - public ChangePropertyEventData(SerializableProperty serializableProperty) + public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity) { SerializableProperty = serializableProperty; + Entity = entity; + } + } + + public readonly struct SetItemStatEventData : IEventData + { + public EventType EventType => EventType.ItemStat; + + public readonly Dictionary Stats; + + public SetItemStatEventData(Dictionary stats) + { + Stats = stats; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 9dd19dc7f..d681e5a8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -47,8 +47,8 @@ namespace Barotrauma CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", - element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); - RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); + element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); + RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); @@ -102,12 +102,13 @@ namespace Barotrauma { public readonly Identifier ItemPrefabIdentifier; - public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab - : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab ?? throw new Exception($"No ItemPrefab with identifier or name \"{ItemPrefabIdentifier}\""); + public ItemPrefab ItemPrefab => + ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab + : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab; public override UInt32 UintIdentifier { get; } - public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + public override IEnumerable ItemPrefabs => ItemPrefab == null ? Enumerable.Empty() : ItemPrefab.ToEnumerable(); public override ItemPrefab FirstMatchingPrefab => ItemPrefab; @@ -122,6 +123,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({ItemPrefabIdentifier})"; + } } public class RequiredItemByTag : RequiredItem @@ -146,6 +152,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({Tag})"; + } } public readonly Identifier TargetItemPrefabIdentifier; @@ -412,7 +423,6 @@ namespace Barotrauma public ImmutableArray Triggers { get; private set; } private ImmutableDictionary treatmentSuitability; - private readonly List fabricationRecipeElements = new List(); /// /// Is this prefab overriding a prefab in another content package @@ -754,6 +764,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DontTransferBetweenSubs { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowHealthBar { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -801,8 +814,6 @@ namespace Barotrauma ? category : MapEntityCategory.Misc; - var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", ""); - //nameidentifier can be used to make multiple items use the same names and descriptions Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); @@ -818,7 +829,7 @@ namespace Barotrauma name = name.Fallback(OriginalName); } - if (parentType == "wrecked") + if (category == MapEntityCategory.Wrecked) { name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } @@ -1145,7 +1156,10 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo != null && priceInfo.CanBeBought && (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) @@ -1157,7 +1171,16 @@ namespace Barotrauma if (priceInfo == null) { continue; } if (!priceInfo.CanBeBought) { continue; } if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } - return true; + 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; } @@ -1242,13 +1265,12 @@ namespace Barotrauma throw new ArgumentException("Both name and identifier cannot be null."); } - ItemPrefab prefab; if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); } - Prefabs.TryGet(identifier, out prefab); + Prefabs.TryGet(identifier, out ItemPrefab prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1296,8 +1318,8 @@ namespace Barotrauma return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags)); } - private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; - private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => + private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs new file mode 100644 index 000000000..4ad18a238 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -0,0 +1,64 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal sealed class ItemStatManager + { + private Item item; + + public ItemStatManager(Item item) + { + this.item = item; + } + + [NetworkSerialize] + public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct + { + public override int GetHashCode() => HashCode.Combine(TalentIdentifier, CharacterID, Stat); + } + + private readonly Dictionary talentStats = new(); + + public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent) + { + if (talent.Character?.ID is not { } characterId || + talent.Prefab?.Identifier is not { } talentIdentifier) + { + return; + } + + TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId); + talentStats[identifier] = value; + +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(item, new Item.SetItemStatEventData(talentStats)); + } +#endif + } + + // Used for getting the value value from network packet + public void ApplyStat(TalentStatIdentifier identifier, float value) + { + talentStats[identifier] = value; + } + + public float GetAdjustedValue(ItemTalentStats stat, float originalValue) + { + float total = originalValue; + foreach (var (key, value) in talentStats) + { + if (key.Stat == stat) + { + total *= value; + } + } + + return total; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 04351e385..fe8dcbd2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Barotrauma.Extensions; namespace Barotrauma { @@ -21,6 +23,8 @@ namespace Barotrauma public bool MatchOnEmpty { get; set; } + public bool RequireEmpty { get; set; } + public bool IgnoreInEditor { get; set; } private ImmutableHashSet excludedIdentifiers; @@ -54,6 +58,20 @@ namespace Barotrauma /// public int TargetSlot = -1; + /// + /// Overrides the position defined in ItemContainer. + /// + public Vector2? ItemPos; + + /// + /// Only affects when ItemContainer.hideItems is false. Doesn't override the value. + /// + public bool? Hide; + + public float Rotation; + + public bool SetActive; + public string JoinedIdentifiers { get { return string.Join(",", Identifiers); } @@ -133,45 +151,60 @@ namespace Barotrauma if (parentItem == null) { return false; } return CheckContained(parentItem); case RelationType.Container: - if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container); + if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty || RequireEmpty; } + return CheckItem(parentItem.Container, this); case RelationType.Equipped: if (character == null) { return false; } - if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } - foreach (Item equippedItem in character.HeldItems) + var heldItems = character.HeldItems; + if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; } + foreach (Item equippedItem in heldItems) { if (equippedItem == null) { continue; } - if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; } + if (CheckItem(equippedItem, this)) + { + if (RequireEmpty && equippedItem.Condition > 0) { return false; } + return true; + } } break; case RelationType.Picked: - if (character == null || character.Inventory == null) { return false; } - foreach (Item pickedItem in character.Inventory.AllItems) + if (character == null) { return false; } + if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; } + var allItems = character.Inventory.AllItems; + if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; } + foreach (Item pickedItem in allItems) { - if (MatchesItem(pickedItem)) { return true; } + if (pickedItem == null) { continue; } + if (CheckItem(pickedItem, this)) + { + if (RequireEmpty && pickedItem.Condition > 0) { return false; } + return true; + } } break; default: return true; } + static bool CheckItem(Item i, RelatedItem ri) => (!ri.ExcludeBroken || ri.RequireEmpty || i.Condition > 0.0f) && (!ri.ExcludeFullCondition || !i.IsFullCondition) && ri.MatchesItem(i); + return false; } private bool CheckContained(Item parentItem) { if (parentItem.OwnInventory == null) { return false; } - - if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty()) + bool isEmpty = parentItem.OwnInventory.IsEmpty(); + if (RequireEmpty && !isEmpty) { return false; } + if (MatchOnEmpty && isEmpty) { return true; } + foreach (var container in parentItem.GetComponents()) { - return true; - } - - foreach (Item contained in parentItem.ContainedItems) - { - 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; } @@ -184,9 +217,21 @@ namespace Barotrauma new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("requireempty", RequireEmpty), new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), - new XAttribute("allowvariants", AllowVariants)); + new XAttribute("allowvariants", AllowVariants), + new XAttribute("rotation", Rotation), + new XAttribute("setactive", SetActive)); + + if (Hide.HasValue) + { + element.Add(new XAttribute(nameof(Hide), Hide.Value)); + } + if (ItemPos.HasValue) + { + element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value)); + } if (excludedIdentifiers.Count > 0) { @@ -249,9 +294,20 @@ namespace Barotrauma RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { ExcludeBroken = element.GetAttributeBool("excludebroken", true), + RequireEmpty = element.GetAttributeBool("requireempty", false), ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), - AllowVariants = element.GetAttributeBool("allowvariants", true) + AllowVariants = element.GetAttributeBool("allowvariants", true), + Rotation = element.GetAttributeFloat("rotation", 0f), + SetActive = element.GetAttributeBool("setactive", false) }; + if (element.GetAttribute(nameof(Hide)) != null) + { + ri.Hide = element.GetAttributeBool(nameof(Hide), false); + } + if (element.GetAttribute(nameof(ItemPos)) != null) + { + ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero); + } string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 8eabe4b03..0770ba69f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -1026,7 +1026,7 @@ namespace Barotrauma.MapCreatures.Behavior branch.DamageVisualizationTimer = 1.0f; } - if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } + if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -1035,7 +1035,7 @@ namespace Barotrauma.MapCreatures.Behavior } if (GameMain.NetworkMember != null) - { + { // damage is handled server side if (GameMain.NetworkMember.IsClient) { @@ -1059,6 +1059,11 @@ namespace Barotrauma.MapCreatures.Behavior if (type == AttackType.Fire) { + if (attacker is not null) + { + damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier); + } + if (IsInWater(branch)) { damage *= 1f - SubmergedWaterResistance; @@ -1066,7 +1071,7 @@ namespace Barotrauma.MapCreatures.Behavior if (defenseCooldown <= 0) { - if (!(StateMachine.State is DefendWithPumpState)) + if (StateMachine.State is not DefendWithPumpState) { StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); defenseCooldown = 180f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 329102f51..b37bf65ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -1,13 +1,12 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -131,17 +130,23 @@ namespace Barotrauma if (damageSource is Item sourceItem) { var launcher = sourceItem.GetComponent()?.Launcher; - displayRange *= - 1.0f - + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + displayRange *= + 1.0f + + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0); - Attack.DamageMultiplier *= - 1.0f + Attack.DamageMultiplier *= + 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0); Attack.SourceItem ??= sourceItem; } + if (attacker is not null) + { + displayRange *= 1f + attacker.GetStatValue(StatTypes.ExplosionRadiusMultiplier); + Attack.DamageMultiplier *= 1f + attacker.GetStatValue(StatTypes.ExplosionDamageMultiplier); + } + Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f); @@ -151,6 +156,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; } @@ -171,13 +180,13 @@ 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; //damage repairable power-consuming items var powered = item.GetComponent(); - if (powered == null || !powered.VulnerableToEMP) continue; + if (powered == null || !powered.VulnerableToEMP) { continue; } if (item.Repairables.Any()) { item.Condition -= item.MaxCondition * EmpStrength * distFactor; @@ -187,7 +196,7 @@ namespace Barotrauma var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge -= powerContainer.Capacity * EmpStrength * distFactor; + powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } } @@ -198,7 +207,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 @@ -355,25 +364,31 @@ namespace Barotrauma } } - AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); - if (attackData.Afflictions != null) + if (attack.Afflictions.Any() || attack.Stun > 0.0f) { - modifiedAfflictions.AddRange(attackData.Afflictions); - } + AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); + if (attackData.Afflictions != null) + { + modifiedAfflictions.AddRange(attackData.Afflictions); + } - //use a position slightly from the limb's position towards the explosion - //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods - Vector2 dir = worldPosition - limb.WorldPosition; - Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); - damages.Add(limb, attackResult.Damage); + //use a position slightly from the limb's position towards the explosion + //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods + Vector2 dir = worldPosition - limb.WorldPosition; + Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); + damages.Add(limb, attackResult.Damage); + } 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); @@ -430,7 +445,7 @@ namespace Barotrauma damagedStructureList.Clear(); foreach (MapEntity entity in MapEntity.mapEntityList) { - if (!(entity is Structure structure)) { continue; } + if (entity is not Structure structure) { continue; } if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; } if (structure.HasBody && @@ -479,7 +494,7 @@ namespace Barotrauma for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { - if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; } + if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; } foreach (var cell in destructibleWall.Cells) { if (cell.IsPointInside(worldPosition)) @@ -502,7 +517,7 @@ namespace Barotrauma return damagedStructures; } - public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) + public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) { List ballastFlorae = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs index ee915e10e..2cc1d7c27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -1,3 +1,6 @@ +using System; +using Barotrauma.Extensions; +using System.Collections.Generic; using System.Collections.Immutable; namespace Barotrauma @@ -11,13 +14,24 @@ 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; + private readonly ImmutableHashSet submarineAvailabilityOverrides; + + public readonly record struct SubmarineAvailability( + Identifier LocationType, + SubmarineClass Class = SubmarineClass.Undefined, + int MaxTier = 0); + public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element)) { OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); @@ -31,9 +45,31 @@ 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); + + var submarineAvailabilityOverrides = new HashSet(); + if (element.GetChildElement("submarines") is ContentXElement availabilityElement) + { + submarineAvailability = GetAvailability(availabilityElement); + foreach (var overrideElement in availabilityElement.GetChildElements("override")) + { + var availabilityOverride = GetAvailability(overrideElement); + submarineAvailabilityOverrides.Add(availabilityOverride); + } + } + this.submarineAvailabilityOverrides = submarineAvailabilityOverrides.ToImmutableHashSet(); + + static SubmarineAvailability GetAvailability(ContentXElement element) + { + return new SubmarineAvailability( + LocationType: element.GetAttributeIdentifier("locationtype", Identifier.Empty), + Class: element.GetAttributeEnum("class", SubmarineClass.Undefined), + MaxTier: element.GetAttributeInt("maxtier", 0)); + } } public static Identifier ParseIdentifier(ContentXElement element) @@ -47,6 +83,31 @@ namespace Barotrauma return identifier; } + public int HighestSubmarineTierAvailable(SubmarineClass subClass, Identifier locationType) + { + if (!submarineAvailability.HasValue) + { + // If the availability is not explicitly defined, make all subs available + return SubmarineInfo.HighestTier; + } + int maxTier = submarineAvailability.Value.MaxTier; + if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == subClass) is SubmarineAvailability locationAndClassOverride) + { + maxTier = locationAndClassOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == locationType && a.Class == SubmarineClass.Undefined) is SubmarineAvailability locationOverride) + { + maxTier = locationOverride.MaxTier; + } + else if (submarineAvailabilityOverrides.FirstOrNull(a => a.LocationType == Identifier.Empty && a.Class == subClass) is SubmarineAvailability classOverride) + { + maxTier = classOverride.MaxTier; + } + return maxTier; + } + + public bool IsSubmarineAvailable(SubmarineInfo info, Identifier locationType) => info.Tier <= HighestSubmarineTierAvailable(info.SubmarineClass, locationType); + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 1665d6a7f..bb19ef11b 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? /// @@ -447,7 +457,7 @@ namespace Barotrauma private Level(LevelData levelData) : base(null, 0) { - this.LevelData = levelData; + LevelData = levelData; borders = new Rectangle(Point.Zero, levelData.Size); } @@ -482,11 +492,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)); @@ -887,6 +894,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 => @@ -2539,7 +2552,8 @@ namespace Barotrauma levelResources.Add((itemPrefab, commonnessInfo)); } else if (itemPrefab.LevelQuantity.TryGetValue(GenerationParams.Identifier, out var fixedQuantityResourceInfo) || - itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) + itemPrefab.LevelQuantity.TryGetValue(LevelData.Biome.Identifier, out fixedQuantityResourceInfo) || + itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) { fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } @@ -3545,6 +3559,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); @@ -3565,7 +3586,7 @@ namespace Barotrauma attemptsLeft--; if (TryGetSpawnPoint(out spawnPoint)) { - success = TryPositionSub(subBorders, subName, ref spawnPoint); + success = TryPositionSub(subBorders, subName, placement, ref spawnPoint); if (success) { break; @@ -3586,10 +3607,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) { @@ -3639,10 +3656,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); @@ -3650,21 +3667,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 { @@ -3694,14 +3711,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) @@ -3730,7 +3747,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; @@ -3756,16 +3773,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; } @@ -3939,7 +3958,7 @@ namespace Barotrauma } SubmarineInfo outpostInfo; - Submarine outpost; + Submarine outpost = null; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) @@ -3953,10 +3972,14 @@ namespace Barotrauma } else { - 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."); @@ -4042,52 +4065,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; @@ -4106,13 +4147,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(); @@ -4123,27 +4163,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) { @@ -4207,7 +4260,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; @@ -4324,6 +4377,10 @@ namespace Barotrauma sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab selectedPrefab = GetCorpsePrefab(usedJobs); + if (selectedPrefab != null) + { + job = selectedPrefab.GetJobPrefab(); + } } } if (selectedPrefab == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4120c334a..48727adfe 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; @@ -57,8 +57,9 @@ namespace Barotrauma /// public int? MinMainPathWidth; - public readonly List EventHistory = new List(); - public readonly List NonRepeatableEvents = new List(); + public readonly List EventHistory = new List(); + public readonly List NonRepeatableEvents = new List(); + public readonly HashSet UsedUniqueSets = new HashSet(); public bool EventsExhausted { get; set; } @@ -137,16 +138,17 @@ namespace Barotrauma Biome = Biome.Prefabs.First(); } - string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); - EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); + string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty()); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); - string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); - NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); + string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty()); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)).Select(p => p.Identifier)); + + UsedUniqueSets = element.GetAttributeIdentifierArray(nameof(UsedUniqueSets), Array.Empty()).ToHashSet(); EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); } - /// /// Instantiates level data using the properties of the connection (seed, size, difficulty) /// @@ -243,6 +245,11 @@ namespace Barotrauma return levelData; } + public void ReassignGenerationParams(string seed) + { + GenerationParams = LevelGenerationParams.GetRandom(seed, Type, Difficulty, Biome.Identifier); + } + public void Save(XElement parentElement) { var newElement = new XElement("Level", @@ -277,13 +284,19 @@ 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))); } } + + if (UsedUniqueSets.Any()) + { + newElement.Add(new XAttribute(nameof(UsedUniqueSets), string.Join(',', UsedUniqueSets))); + } + parentElement.Add(newElement); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 498702fa1..7ddfd6134 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -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) { @@ -661,6 +732,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/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 8e30125de..5b9d587fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -542,38 +542,41 @@ namespace Barotrauma GlobalForceDecreaseTimer = 0.0f; } - foreach (LevelObject obj in updateableObjects) + if (updateableObjects is not null) { - if (GameMain.NetworkMember is { IsServer: true }) + foreach (LevelObject obj in updateableObjects) { - obj.NetworkUpdateTimer -= deltaTime; - if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); - obj.NeedsNetworkSyncing = false; - obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; - } - } - if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - - if (obj.Triggers != null) - { - obj.ActivePrefab = obj.Prefab; - for (int i = 0; i < obj.Triggers.Count; i++) - { - obj.Triggers[i].Update(deltaTime); - if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + obj.NetworkUpdateTimer -= deltaTime; + if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) { - obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); + obj.NeedsNetworkSyncing = false; + obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } } - } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - if (obj.PhysicsBody != null) - { - if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } - /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); - obj.Rotation = -obj.PhysicsBody.Rotation;*/ + if (obj.Triggers != null) + { + obj.ActivePrefab = obj.Prefab; + for (int i = 0; i < obj.Triggers.Count; i++) + { + obj.Triggers[i].Update(deltaTime); + if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + { + obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + } + } + } + + if (obj.PhysicsBody != null) + { + if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } + /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); + obj.Rotation = -obj.PhysicsBody.Rotation;*/ + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index a2ad86140..213fad7bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -287,6 +287,13 @@ namespace Barotrauma private set; } + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] + public Color SpriteColor + { + get; + private set; + } + public string Name => Identifier.Value; public List ChildObjects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 480163ffa..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); } @@ -661,7 +661,7 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effect.type, deltaTime, triggerer, targets); } } @@ -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 52bc59f55..107fe5125 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 2efccc7e5..c734d190c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -61,7 +61,7 @@ namespace Barotrauma private LocationType addInitialMissionsForType; - public bool Discovered { get; private set; } + public bool Discovered => GameMain.GameSession?.Map?.IsDiscovered(this) ?? false; public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; @@ -83,7 +83,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; } @@ -135,7 +139,7 @@ namespace Barotrauma foreach (var stockElement in storeElement.GetChildElements("stock")) { var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty); - if (identifier.IsEmpty || !(ItemPrefab.FindByIdentifier(identifier) is ItemPrefab prefab)) { continue; } + if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; } int qty = stockElement.GetAttributeInt("qty", 0); if (qty < 1) { continue; } Stock.Add(new PurchasedItem(prefab, qty, buyer: null)); @@ -157,7 +161,7 @@ namespace Barotrauma foreach (var childElement in element.GetChildElements("item")) { var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); - if (id.IsEmpty || !(ItemPrefab.FindByIdentifier(id) is ItemPrefab prefab)) { continue; } + if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; } specials.Add(prefab); } return specials; @@ -240,7 +244,7 @@ namespace Barotrauma availableStock.Add(stockItem.ItemPrefab, weight); } DailySpecials.Clear(); - int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } @@ -283,6 +287,18 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(true); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (characters.Any()) + { + if (Location.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive) + { + 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))); + } // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -303,6 +319,14 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(false); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (characters.Any()) + { + price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier)); + price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); + } + // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -465,7 +489,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); @@ -478,13 +502,23 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - Discovered = element.GetAttributeBool("discovered", false); - 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) { @@ -641,7 +675,7 @@ namespace Barotrauma return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } - public void ChangeType(LocationType newType) + public void ChangeType(CampaignMode campaign, LocationType newType) { if (newType == Type) { return; } @@ -656,34 +690,49 @@ 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) { - 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); + 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; } + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } var mission = InstantiateMission(missionPrefab, connection); + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); @@ -693,7 +742,12 @@ namespace Barotrauma public void UnlockMission(MissionPrefab missionPrefab) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } + if (AvailableMissions.Any(m => !m.Prefab.AllowOtherMissionsInLevel)) { return; } var mission = InstantiateMission(missionPrefab); + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); @@ -703,6 +757,7 @@ namespace Barotrauma 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) @@ -717,6 +772,10 @@ namespace Barotrauma { return null; } + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); @@ -728,7 +787,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."); @@ -750,6 +810,10 @@ namespace Barotrauma { return null; } + if (!mission.Prefab.AllowOtherMissionsInLevel) + { + availableMissions.Clear(); + } availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); @@ -960,12 +1024,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(); @@ -1050,13 +1124,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); } @@ -1125,7 +1207,7 @@ namespace Barotrauma public void UpdateStores() { // In multiplayer, stores should be updated by the server and loaded from save data by clients - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } if (Stores == null) { CreateStores(); @@ -1167,13 +1249,10 @@ namespace Barotrauma stockToRemove.ForEach(i => stock.Remove(i)); store.Stock.Clear(); store.Stock.AddRange(stock); - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || store.DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) - { - store.GenerateSpecials(); - } store.GeneratePriceModifier(); } + + StepsSinceSpecialsUpdated++; foreach (var identifier in storesToRemove) { Stores.Remove(identifier); @@ -1184,6 +1263,20 @@ namespace Barotrauma } } + public void UpdateSpecials() + { + if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; } + + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + + foreach (StoreInfo store in Stores.Values) + { + if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; } + + store.GenerateSpecials(); + } + } + private void UpdateStoreIdentifiers() { StoreIdentifiers.Clear(); @@ -1231,6 +1324,7 @@ namespace Barotrauma public float GetStoreReputationModifier(bool buying) { + if (Reputation == null) { return 1.0f; } if (buying) { if (Reputation.Value > 0.0f) @@ -1255,35 +1349,37 @@ namespace Barotrauma } } - public int GetExtraSpecialSalesCount() + public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - public void Discover(bool checkTalents = true) + public int HighestSubmarineTierAvailable(SubmarineClass submarineClass) { - if (Discovered) { return; } - Discovered = true; - if (checkTalents) - { - GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); - } + if (!HasOutpost()) { return 0; } + return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier; } - public void Reset() + public int HighestSubmarineTierAvailable() => HighestSubmarineTierAvailable(SubmarineClass.Undefined); + + public bool IsSubmarineAvailable(SubmarineInfo info) + { + return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true; + } + + public void Reset(CampaignMode campaign) { if (Type != OriginalType) { - ChangeType(OriginalType); + ChangeType(campaign, OriginalType); PendingLocationTypeChange = null; } CreateStores(force: true); ClearMissions(); LevelData?.EventHistory?.Clear(); UnlockInitialMissions(); - Discovered = false; } public XElement Save(Map map, XElement parentElement) @@ -1294,7 +1390,6 @@ namespace Barotrauma new XAttribute("basename", BaseName), new XAttribute("name", Name), new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty), - new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), @@ -1302,6 +1397,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++) @@ -1423,7 +1528,7 @@ namespace Barotrauma HireManager?.Remove(); } - class AbilityLocation : AbilityObject, IAbilityLocation + public class AbilityLocation : AbilityObject, IAbilityLocation { public AbilityLocation(Location location) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 97651462a..64436c233 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -24,6 +24,9 @@ namespace Barotrauma public readonly Dictionary MinCountPerZone = new Dictionary(); public readonly LocalizedString Name; + public readonly LocalizedString Description; + + public readonly LocalizedString ForceLocationName; public readonly float BeaconStationChance; @@ -38,7 +41,13 @@ namespace Barotrauma public bool IsEnterable { get; private set; } - public bool UseInMainMenu + public bool UsePortraitInMainMenu + { + get; + private set; + } + + public bool UsePortraitInRandomLoadingScreens { get; private set; @@ -70,6 +79,13 @@ namespace Barotrauma public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } + private readonly Identifier forceOutpostGenerationParamsIdentifier; + + /// + /// If set to true, only event sets that explicitly define this location type in can be selected at this location. Defaults to false. + /// + public bool IgnoreGenericEvents { get; } + public Color SpriteColor { get; @@ -88,6 +104,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 + ")"; @@ -96,13 +114,17 @@ namespace Barotrauma public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { Name = TextManager.Get("LocationName." + Identifier, "unknown"); + Description = TextManager.Get("LocationDescription." + Identifier, ""); BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); + UsePortraitInMainMenu = element.GetAttributeBool(nameof(UsePortraitInMainMenu), element.GetAttributeBool("useinmainmenu", false)); + UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + ShowSonarMarker = element.GetAttributeBool("showsonarmarker", true); + MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); @@ -110,26 +132,37 @@ namespace Barotrauma ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty); + forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty); + + IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false); + 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" }); + 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"); + } } string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); @@ -261,6 +294,15 @@ namespace Barotrauma } } + public OutpostGenerationParams GetForcedOutpostGenerationParams() + { + if (OutpostGenerationParams.OutpostParams.TryGet(forceOutpostGenerationParamsIdentifier, out var parameters)) + { + return parameters; + } + return null; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 0639ceb6f..6f6c91070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -141,24 +141,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(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index e87fd88da..504ee1646 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; } @@ -68,10 +69,15 @@ namespace Barotrauma public List Locations { get; private set; } + private readonly List locationsDiscovered = new List(); + private readonly List outpostsVisited = new List(); + public List Connections { get; private set; } public Radiation Radiation; + private bool wasLocationDiscoveryOrderTracked = true; + public Map(CampaignSettings settings) { generationParams = MapGenerationParams.Instance; @@ -112,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) @@ -122,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()) @@ -182,23 +183,73 @@ 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]); + Locations[endLocationIndex].LevelData.ReassignGenerationParams(Seed); + } + 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, 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)) { @@ -209,6 +260,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); } @@ -226,18 +288,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; } @@ -258,6 +315,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; + } + } + } } } @@ -282,7 +353,12 @@ namespace Barotrauma } } - CurrentLocation.Discover(true); + if (campaign.IsSinglePlayer && campaign.Settings.TutorialEnabled && LocationType.Prefabs.TryGet("tutorialoutpost", out var tutorialOutpost)) + { + CurrentLocation.ChangeType(campaign, tutorialOutpost); + } + Discover(CurrentLocation); + Visit(CurrentLocation); CurrentLocation.CreateStores(); foreach (var location in Locations) @@ -297,7 +373,7 @@ namespace Barotrauma #region Generation - private void Generate(CampaignSettings settings) + private void Generate(CampaignMode campaign) { Connections.Clear(); Locations.Clear(); @@ -515,12 +591,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); @@ -528,9 +606,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); } @@ -542,10 +620,17 @@ 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")); } 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]; + } } } @@ -613,20 +698,26 @@ namespace Barotrauma } } - CreateEndLocation(); - foreach (Location location in Locations) { location.LevelData = new LevelData(location, 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); + } } + 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; @@ -695,18 +786,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; } } @@ -720,17 +811,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); + previousToEndLocation.ChangeType(campaign, locationType); } //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--) { @@ -741,7 +854,10 @@ namespace Barotrauma otherLocation?.Connections.Remove(connection); Connections.Remove(connection); } - Locations.RemoveAt(i); + if (!endLocations.Contains(Locations[i])) + { + Locations.RemoveAt(i); + } } } @@ -758,22 +874,38 @@ 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++) + { + var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i); + if (outpostParams != null) + { + endLocations[i].LevelData.ForceOutpostGenerationParams = outpostParams; + } + } } private void ExpandBiomes(List seeds) @@ -807,20 +939,44 @@ namespace Barotrauma { 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; - CurrentLocation.Discover(); + Discover(CurrentLocation); + Visit(CurrentLocation); SelectedLocation = null; CurrentLocation.CreateStores(); @@ -851,7 +1007,7 @@ namespace Barotrauma Location prevLocation = CurrentLocation; CurrentLocation = Locations[index]; - CurrentLocation.Discover(); + Discover(CurrentLocation); CurrentLocation.CreateStores(); if (prevLocation != CurrentLocation) @@ -966,7 +1122,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)); @@ -979,13 +1135,23 @@ namespace Barotrauma steps = Math.Min(steps, 5); for (int i = 0; i < steps; i++) { - ProgressWorld(); + ProgressWorld(campaign); + } + + // always update specials every step + for (int i = 0; i < Math.Max(1, steps); i++) + { + foreach (Location location in Locations) + { + if (!location.Discovered) { continue; } + location.UpdateSpecials(); + } } Radiation?.OnStep(steps); } - private void ProgressWorld() + private void ProgressWorld(CampaignMode campaign) { foreach (Location location in Locations) { @@ -1013,14 +1179,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--; @@ -1040,7 +1206,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); } } } @@ -1073,7 +1239,7 @@ namespace Barotrauma } else { - return ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(campaign, location, selectedTypeChange); } return false; } @@ -1143,7 +1309,7 @@ namespace Barotrauma return distance; } - private bool ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change) { string prevName = location.Name; @@ -1158,7 +1324,7 @@ namespace Barotrauma { location.ClearMissions(); } - location.ChangeType(newType); + location.ChangeType(campaign, newType); ChangeLocationTypeProjSpecific(location, prevName, change); foreach (var requirement in change.Requirements) { @@ -1174,13 +1340,58 @@ namespace Barotrauma partial void ClearAnimQueue(); + public void Discover(Location location, bool checkTalents = true) + { + if (location is null) { return; } + if (locationsDiscovered.Contains(location)) { return; } + locationsDiscovered.Add(location); + if (checkTalents) + { + GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Location.AbilityLocation(location))); + } + } + + public void Visit(Location location) + { + if (location is null) { return; } + if (!location.HasOutpost()) { return; } + if (outpostsVisited.Contains(location)) { return; } + outpostsVisited.Add(location); + } + + public void ClearLocationHistory() + { + locationsDiscovered.Clear(); + outpostsVisited.Clear(); + } + + public int? GetDiscoveryIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return locationsDiscovered.IndexOf(location); + } + + public int? GetVisitIndex(Location location) + { + if (!wasLocationDiscoveryOrderTracked) { return null; } + if (location is null) { return -1; } + return outpostsVisited.IndexOf(location); + } + + public bool IsDiscovered(Location location) + { + if (location is null) { return false; } + return locationsDiscovered.Contains(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 @@ -1190,17 +1401,18 @@ 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; } + ClearLocationHistory(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1216,26 +1428,19 @@ namespace Barotrauma } } location.LoadLocationTypeChange(subElement); + + // Backwards compatibility if (subElement.GetAttributeBool("discovered", false)) { - location.Discover(checkTalents: false); - } - if (location.Discovered) - { -#if CLIENT - RemoveFogOfWar(location); -#endif - if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) - { - furthestDiscoveredLocation = location; - } + Discover(location); + wasLocationDiscoveryOrderTracked = 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); @@ -1246,6 +1451,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); @@ -1258,6 +1469,36 @@ namespace Barotrauma case "radiation": Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; + case "discovered": + 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); + } + 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); + } + break; + } + } + + 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; } } @@ -1266,6 +1507,27 @@ namespace Barotrauma location?.InstantiateLoadedMissions(this); } +#if RELEASE + TODO: MAKE SURE THE VERSION NUMBER BELOW IS CORRECT FOR THE FULL RELEASE (OR WHICHEVER UPDATE WE ADD THE FACTIONS IN) +#endif + //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) { @@ -1304,7 +1566,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++) @@ -1333,6 +1595,30 @@ namespace Barotrauma mapElement.Add(Radiation.Save()); } + if (locationsDiscovered.Any()) + { + var discoveryElement = new XElement("discovered"); + foreach (Location location in locationsDiscovered) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + discoveryElement.Add(locationElement); + } + mapElement.Add(discoveryElement); + } + + if (outpostsVisited.Any()) + { + var visitElement = new XElement("visited"); + foreach (Location location in outpostsVisited) + { + int index = Locations.IndexOf(location); + var locationElement = new XElement("location", new XAttribute("i", index)); + visitElement.Add(locationElement); + } + mapElement.Add(visitElement); + } + element.Add(mapElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e8489c131..443c45743 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -158,7 +158,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; } 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 6e9b95be9..0b21bd0d1 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,26 +116,38 @@ namespace Barotrauma set; } + public LevelData.LevelType? LevelType + { + get; + set; + } + [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } + public ContentPath OutpostFilePath { get; set; } + public class ModuleCount { public Identifier Identifier; 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; } } @@ -130,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 @@ -148,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() { @@ -165,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; } @@ -182,8 +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()) @@ -196,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); @@ -251,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 ImmutableHashSet GetStoreIdentifiers() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 30217e2a5..23d869ccc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -143,9 +143,9 @@ namespace Barotrauma //select which module types the outpost should consist of List pendingModuleFlags = onlyEntrance ? - generationParams.ModuleCounts.First().Identifier.ToEnumerable().ToList() : - SelectModules(outpostModules, generationParams); - + (generationParams.ModuleCounts.FirstOrDefault()?.Identifier.ToEnumerable() ?? Enumerable.Empty()).ToList() : + SelectModules(outpostModules, location, generationParams); + foreach (Identifier flag in pendingModuleFlags) { if (flag == "none") { continue; } @@ -237,15 +237,21 @@ 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()) + .Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path)) .OrderBy(f => f.UintIdentifier).ToArray(); if (!outpostFiles.Any()) { @@ -258,6 +264,7 @@ namespace Barotrauma sub = new Submarine(prebuiltOutpostInfo); sub.Info.OutpostGenerationParams = generationParams; location?.RemoveTakenItems(); + EnableFactionSpecificEntities(sub, location); return sub; List loadEntities(Submarine sub) @@ -296,18 +303,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) { @@ -396,6 +412,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) @@ -426,7 +459,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(); @@ -437,23 +470,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 @@ -696,6 +735,14 @@ namespace Barotrauma rect.Location += (module.Offset + module.MoveOffset).ToPoint(); rect.Y += module.Bounds.Height; + Vector2? selfGapPos1 = null; + Vector2? selfGapPos2 = null; + if (module.PreviousModule != null) + { + selfGapPos1 = module.Offset + module.ThisGap.Position + module.MoveOffset; + selfGapPos2 = module.PreviousModule.Offset + module.PreviousGap.Position + module.PreviousModule.MoveOffset; + } + foreach (PlacedModule otherModule in modules) { if (otherModule == module || otherModule.PreviousModule == null || otherModule.PreviousModule == module) { continue; } @@ -710,7 +757,17 @@ namespace Barotrauma Vector2 gapPos1 = otherModule.Offset + otherModule.ThisGap.Position + gapEdgeOffset + otherModule.MoveOffset; Vector2 gapPos2 = otherModule.PreviousModule.Offset + otherModule.PreviousGap.Position + gapEdgeOffset + otherModule.PreviousModule.MoveOffset; - if (Submarine.RectContains(rect, gapPos1) || Submarine.RectContains(rect, gapPos2) || MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + if (Submarine.RectContains(rect, gapPos1) || + Submarine.RectContains(rect, gapPos2) || + MathUtils.GetLineRectangleIntersection(gapPos1, gapPos2, rect, out _)) + { + return true; + } + + //check if the connection overlaps with this module's connection + if (selfGapPos1.HasValue && selfGapPos2.HasValue && + !gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) && + MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value)) { return true; } @@ -1362,6 +1419,21 @@ 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]; + } + } + } + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) @@ -1564,7 +1636,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; } @@ -1583,7 +1660,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; @@ -1605,11 +1682,11 @@ 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 96a8e9028..696eda83f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -27,11 +27,20 @@ namespace Barotrauma public bool DisplayNonEmpty { get; } = false; public Identifier StoreIdentifier { get; } + public bool RequiresUnlock { get; } + /// /// Used when both and are set to 0. /// 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 @@ -48,11 +57,12 @@ namespace Barotrauma int maxAmount = GetMaxAmount(element); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); + RequiresUnlock = element.GetAttributeBool("requiresunlock", false); } public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, - bool displayNonEmpty = false, string storeIdentifier = null) + bool displayNonEmpty = false, bool requiresUnlock = false, string storeIdentifier = null) { Price = price; CanBeBought = canBeBought; @@ -64,6 +74,20 @@ namespace Barotrauma CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; StoreIdentifier = new Identifier(storeIdentifier); + 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) @@ -78,6 +102,7 @@ namespace Barotrauma float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true)); + bool requiresUnlock = element.GetAttributeBool("requiresunlock", false); foreach (XElement childElement in element.GetChildElements("price")) { float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); @@ -91,26 +116,30 @@ namespace Barotrauma } string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier); // TODO: Add some error messages if we have defined the min or max amount while the item is not sold - var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), - sold, - sold ? GetMinAmount(childElement, minAmount) : 0, - sold ? GetMaxAmount(childElement, maxAmount) : 0, - canBeSpecial, - storeMinLevelDifficulty, - storeBuyingMultiplier, - displayNonEmpty, - storeIdentifier); + var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice), + canBeBought: sold, + minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, + maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: storeMinLevelDifficulty, + buyingPriceMultiplier: storeBuyingMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock, + storeIdentifier: storeIdentifier); + priceInfo.LoadReputationRestrictions(childElement); priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); - defaultPrice = new PriceInfo(basePrice, - soldElsewhere, - soldElsewhere ? minAmount : 0, - soldElsewhere ? maxAmount : 0, - canBeSpecial, - minLevelDifficulty, - buyingPriceMultiplier, - displayNonEmpty); + defaultPrice = new PriceInfo(price: basePrice, + canBeBought: soldElsewhere, + minAmount: soldElsewhere ? minAmount : 0, + maxAmount: soldElsewhere ? maxAmount : 0, + canBeSpecial: canBeSpecial, + minLevelDifficulty: minLevelDifficulty, + buyingPriceMultiplier: buyingPriceMultiplier, + displayNonEmpty: displayNonEmpty, + requiresUnlock: requiresUnlock); + defaultPrice.LoadReputationRestrictions(element); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 3dd5eea35..f55c7f6a0 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 5ae404712..31dd82776 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; } @@ -1114,7 +1171,8 @@ namespace Barotrauma { if (item.Submarine != this) { continue; } var pump = item.GetComponent(); - if (pump == null || !item.HasTag("ballast") || item.CurrentHull == null) { continue; } + if (pump == null || item.CurrentHull == null) { continue; } + if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } pump.FlowPercentage = 0.0f; ballastHulls.Add(item.CurrentHull); } @@ -1388,7 +1446,6 @@ namespace Barotrauma if (info.IsOutpost) { ShowSonarMarker = false; - PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = CharacterTeamType.FriendlyNPC; bool indestructible = @@ -1459,10 +1516,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index b9af39add..62f2fa541 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; @@ -146,9 +153,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 +196,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); @@ -623,7 +641,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 426941fe8..b1885e71b 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; @@ -314,6 +315,7 @@ namespace Barotrauma Tier = original.Tier; IsManuallyOutfitted = original.IsManuallyOutfitted; Tags = original.Tags; + OutpostGenerationParams = original.OutpostGenerationParams; if (original.OutpostModuleInfo != null) { OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo); @@ -554,6 +556,14 @@ namespace Barotrauma } return realWorldCrushDepth; } + 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) @@ -747,6 +757,36 @@ namespace Barotrauma return doc; } - public static int GetDefaultTier(int price) => price > 20000 ? 3 : price > 10000 ? 2 : 1; + 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, characterList) 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..60a68357b 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 { @@ -54,6 +54,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 +146,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 @@ -1018,7 +1026,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 +1043,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 +1085,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/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 6e5300c4c..8bde51456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -27,7 +27,8 @@ namespace Barotrauma.Networking SellSubItems = 0x4000, ManageMap = 0x8000, ManageHires = 0x10000, - All = 0x1FFFF + ManageBotTalents = 0x20000, + All = 0x3FFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 6f68b3730..15a041953 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -258,7 +258,7 @@ namespace Barotrauma.Networking var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge = powerContainer.Capacity; + powerContainer.Charge = powerContainer.GetCapacity(); } var door = item.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index cbf0474b8..e4cbc1bb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -382,12 +382,17 @@ namespace Barotrauma.Networking private bool autoRestart; - public bool IsPublic; - private int maxPlayers; public List ClientPermissions { get; private set; } = new List(); + [Serialize(true, IsPropertySaveable.Yes)] + public bool IsPublic + { + get; + set; + } + private int tickRate = 20; [Serialize(20, IsPropertySaveable.Yes)] public int TickRate @@ -518,13 +523,20 @@ namespace Barotrauma.Networking } } - [Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)] + [Serialize(LosMode.Opaque, IsPropertySaveable.Yes)] public LosMode LosMode { get; set; } + [Serialize(EnemyHealthBarMode.ShowAll, IsPropertySaveable.Yes)] + public EnemyHealthBarMode ShowEnemyHealthBars + { + get; + set; + } + [Serialize(800, IsPropertySaveable.Yes)] public int LinesPerLogFile { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index ee9831e4d..31ee2c9be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -97,7 +97,6 @@ namespace Barotrauma get { return list; } } - protected Vector2 prevPosition; protected float prevRotation; @@ -120,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; @@ -344,6 +345,22 @@ namespace Barotrauma } } + /// + /// Ignore rotation calls for the rest of this and the next update. Automatically disabled after that. Used for temporarily suppressing the SmoothRotate calls to prevent conflicting or unitentionally amplified rotations. + /// + public bool SuppressSmoothRotationCalls + { + get => _suppressSmoothRotationCalls; + set + { + _suppressSmoothRotationCalls = value; + smoothRotationSuppressionCounter = 0; + } + } + + private bool _suppressSmoothRotationCalls; + private int smoothRotationSuppressionCounter; + public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { } public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { } public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { } @@ -370,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); @@ -419,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; @@ -457,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; } @@ -477,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(); @@ -499,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(); } @@ -516,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(); } @@ -533,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(); @@ -815,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; } @@ -831,6 +856,17 @@ namespace Barotrauma } drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor); rotationOffset = NetConfig.InterpolateRotationError(rotationOffset); + if (SuppressSmoothRotationCalls) + { + if (smoothRotationSuppressionCounter > 0) + { + SuppressSmoothRotationCalls = false; + } + else + { + smoothRotationSuppressionCounter++; + } + } } public void UpdateDrawPosition() @@ -873,6 +909,7 @@ namespace Barotrauma /// Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f. public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true) { + if (SuppressSmoothRotationCalls) { return; } float nextAngle = FarseerBody.Rotation + FarseerBody.AngularVelocity * (float)Timing.Step; float angle = wrapAngle ? MathUtils.GetShortestAngle(nextAngle, targetRotation) : @@ -881,7 +918,7 @@ namespace Barotrauma if (FarseerBody.BodyType == BodyType.Kinematic) { - if (!IsValidValue(torque, "torque")) return; + if (!IsValidValue(torque, "torque")) { return; } FarseerBody.AngularVelocity = torque; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 93fa4d6a7..4b3b7edc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; @@ -256,7 +257,7 @@ namespace Barotrauma /// Prefab identifier /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not - public bool TryGet(Identifier identifier, out T? result) + public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) { Prefab.DisallowCallFromConstructor(); if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 2b8e658f9..226e1c3c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -65,7 +65,7 @@ namespace Barotrauma } #endif #if CLIENT - (botCountText as GUITextBlock).Text = botCount.ToString(); + botCountText.Text = botCount.ToString(); #endif } @@ -79,7 +79,13 @@ namespace Barotrauma } #endif #if CLIENT - (botSpawnModeText as GUITextBlock).Text = TextManager.Get(botSpawnMode.ToString()); + + botSpawnModeText.Text = TextManager.Get(botSpawnMode.ToString()); + botSpawnModeText.ToolTip = TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip") + "\n\n" + TextManager.Get("botspawn.campaignnote"); + foreach (var btn in botSpawnModeButtons) + { + btn.ToolTip = botSpawnModeText.ToolTip; + } #endif } @@ -89,7 +95,7 @@ namespace Barotrauma if (GameMain.Server != null) GameMain.Server.ServerSettings.TraitorsEnabled = enabled; #endif #if CLIENT - (traitorProbabilityText as GUITextBlock).Text = TextManager.Get(enabled.ToString()); + traitorProbabilityText.Text = TextManager.Get(enabled.ToString()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 16e5022f4..5cd9c0579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -214,6 +214,7 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false) @@ -221,6 +222,12 @@ namespace Barotrauma ?? defaultValue; } + public static ImmutableHashSet GetAttributeIdentifierImmutableHashSet(this XElement element, string key, ImmutableHashSet defaultValue, bool trim = true) + { + return element.GetAttributeIdentifierArray(key, null, trim)?.ToImmutableHashSet() + ?? defaultValue; + } + public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { if (element == null) { return defaultValue; } @@ -769,7 +776,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/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index e1ec4e598..b327c3903 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -35,10 +35,19 @@ namespace Barotrauma Activity } + public enum EnemyHealthBarMode + { + ShowAll, + BossHealthBarsOnly, + HideAll + } + public static class GameSettings { public struct Config { + public const float DefaultAimAssist = 0.05f; + public static Config GetDefault() { Config config = new Config @@ -50,7 +59,8 @@ namespace Barotrauma SubEditorBackground = new Color(13, 37, 69, 255), EnableSplashScreen = true, PauseOnFocusLost = true, - AimAssistAmount = 0.5f, + AimAssistAmount = DefaultAimAssist, + ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll, EnableMouseLook = true, ChatOpen = true, CrewMenuOpen = true, @@ -110,6 +120,7 @@ namespace Barotrauma public LanguageIdentifier Language; public bool VerboseLogging; public bool SaveDebugConsoleLogs; + public string SavePath; public int SubEditorUndoBuffer; public int MaxAutoSaves; public int AutoSaveIntervalSeconds; @@ -118,6 +129,7 @@ namespace Barotrauma public bool PauseOnFocusLost; public float AimAssistAmount; public bool EnableMouseLook; + public EnemyHealthBarMode ShowEnemyHealthBars; public bool ChatOpen; public bool CrewMenuOpen; public bool EditorDisclaimerShown; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index f86226e55..b831c7670 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -24,7 +24,8 @@ namespace Barotrauma HasSpecifierTag, Affliction, EntityType, - LimbType + LimbType, + SkillRequirement } public enum Comparison @@ -73,6 +74,7 @@ namespace Barotrauma case "targetcontainer": case "targetgrandparent": case "targetcontaineditem": + case "skillrequirement": return false; default: return true; @@ -110,6 +112,11 @@ namespace Barotrauma { Type = ConditionType.Uncertain; } + + if (attribute.Parent.GetAttributeBool("skillrequirement", false)) + { + Type = ConditionType.SkillRequirement; + } AttributeValue = valueString; SplitAttributeValue = valueString.Split(','); @@ -305,25 +312,20 @@ namespace Barotrauma if (health == null) { return false; } var affliction = health.GetAffliction(AttributeName.ToIdentifier()); float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - if (FloatValue.HasValue) - { - float value = FloatValue.Value; - switch (Operator) - { - case OperatorType.Equals: - return afflictionStrength == value; - case OperatorType.GreaterThan: - return afflictionStrength > value; - case OperatorType.GreaterThanEquals: - return afflictionStrength >= value; - case OperatorType.LessThan: - return afflictionStrength < value; - case OperatorType.LessThanEquals: - return afflictionStrength <= value; - case OperatorType.NotEquals: - return afflictionStrength != value; - } - } + + return ValueMatchesRequirement(afflictionStrength); + } + } + return false; + case ConditionType.SkillRequirement: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + if (target is Character targetChar) + { + float skillLevel = targetChar.GetSkillLevel(AttributeName.ToIdentifier()); + + return ValueMatchesRequirement(skillLevel); } } return false; @@ -332,6 +334,30 @@ namespace Barotrauma } } + private bool ValueMatchesRequirement(float testedValue) + { + if (FloatValue.HasValue) + { + float value = FloatValue.Value; + switch (Operator) + { + case OperatorType.Equals: + return testedValue == value; + case OperatorType.GreaterThan: + return testedValue > value; + case OperatorType.GreaterThanEquals: + return testedValue >= value; + case OperatorType.LessThan: + return testedValue < value; + case OperatorType.LessThanEquals: + return testedValue <= value; + case OperatorType.NotEquals: + return testedValue != value; + } + } + return false; + } + private bool MatchesTagCondition(ISerializableEntity target) { if (!(target is Item item)) { return Operator == OperatorType.NotEquals; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 10c7c7527..ba975f3a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,6 +1,7 @@ using Barotrauma.Abilities; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -278,6 +279,13 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } + /// + /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. + /// Also the creatures spawned by other means are counted in the check. + /// + [Serialize(0, IsPropertySaveable.No)] + public int TotalMaxCount { get; private set; } + [Serialize(0, IsPropertySaveable.No)] public int Stun { get; private set; } @@ -337,6 +345,8 @@ namespace Barotrauma public float intervalTimer; + private readonly bool oneShot; + public static readonly List DurationList = new List(); /// @@ -362,7 +372,7 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem, dropContainedItems, removeCharacter, breakLimb, hideLimb; + private readonly bool removeItem, dropContainedItems, dropItem, removeCharacter, breakLimb, hideLimb; private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -379,7 +389,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; @@ -457,6 +468,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) @@ -608,6 +621,9 @@ namespace Barotrauma propertyAttributes.Add(attribute); } break; + case "oneshot": + oneShot = attribute.GetAttributeBool(false); + break; default: propertyAttributes.Add(attribute); break; @@ -656,6 +672,9 @@ namespace Barotrauma case "dropcontaineditems": dropContainedItems = true; break; + case "dropitem": + dropItem = true; + break; case "removecharacter": removeCharacter = true; break; @@ -866,10 +885,9 @@ namespace Barotrauma return true; } - public IReadOnlyList GetNearbyTargets(Vector2 worldPosition, List targets = null) + public void AddNearbyTargets(Vector2 worldPosition, List targets) { - targets ??= new List(); - if (Range <= 0.0f) { return targets; } + if (Range <= 0.0f) { return; } if (HasTargetType(TargetType.NearbyCharacters)) { foreach (Character c in Character.CharacterList) @@ -906,7 +924,6 @@ namespace Barotrauma } } } - return targets; bool CheckDistance(ISpatialEntity e) { @@ -1113,6 +1130,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; } @@ -1137,6 +1155,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 (intervalTimer > 0.0f) @@ -1241,6 +1260,7 @@ namespace Barotrauma protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { + if (Disabled) { return; } if (lifeTime > 0) { lifeTimer -= deltaTime; @@ -1282,6 +1302,16 @@ namespace Barotrauma } } + if (dropItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) + { + item.Drop(dropper: null); + } + } + } if (dropContainedItems) { for (int i = 0; i < targets.Count; i++) @@ -1296,6 +1326,13 @@ namespace Barotrauma } } } + else if (targets[i] is Character character && character.Inventory != null) + { + foreach (var containedItem in character.Inventory.AllItemsMod) + { + containedItem.Drop(dropper: null); + } + } } } if (removeItem) @@ -1333,17 +1370,20 @@ namespace Barotrauma if (limb.body == sourceBody) { targetLimb = limb; - if (breakLimb) - { - character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); - } break; } } } - if (hideLimb) + if (targetLimb != null) { - targetLimb?.HideAndDisable(hideLimbTimer); + if (breakLimb) + { + targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, ignoreSeveranceProbabilityModifier: true, attacker: user); + } + if (hideLimb) + { + targetLimb.HideAndDisable(hideLimbTimer); + } } } } @@ -1475,6 +1515,9 @@ namespace Barotrauma targetCharacter = targetLimb.character; } } + + Character entityCharacter = entity as Character; + targetCharacter ??= entityCharacter; if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer) { if (targetCharacter.AIController is EnemyAIController enemyAI) @@ -1482,7 +1525,13 @@ namespace Barotrauma foreach (AITrigger trigger in aiTriggers) { if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; } - if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb != targetLimb) { continue; } + if (entityCharacter != targetCharacter) + { + if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb is Limb hitLimb) + { + if (hitLimb != targetLimb) { continue; } + } + } if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; } enemyAI.LaunchTrigger(trigger); break; @@ -1580,18 +1629,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 }); + } } } } @@ -1606,6 +1657,14 @@ namespace Barotrauma Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { + if (characterSpawnInfo.TotalMaxCount > 0) + { + if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) + { + Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); + return; + } + } if (newCharacter.AIController is EnemyAIController enemyAi && enemyAi.PetBehavior != null && entity is Item item && @@ -1666,11 +1725,11 @@ namespace Barotrauma Character.Controlled = newCharacter; } #elif SERVER - /*foreach (Client c in GameMain.Server.ConnectedClients) + foreach (Client c in GameMain.Server.ConnectedClients) { if (c.Character != target) { continue; } GameMain.Server.SetClientCharacter(c, newCharacter); - }*/ + } #endif } if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } @@ -1717,7 +1776,19 @@ namespace Barotrauma } float spread = Rand.Range(-chosenItemSpawnInfo.AimSpreadRad, chosenItemSpawnInfo.AimSpreadRad); float rotation = chosenItemSpawnInfo.RotationRad; - Vector2 spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; + Vector2 worldPos; + if (sourceBody != null) + { + worldPos = sourceBody.Position; + if (user?.Submarine != null) + { + worldPos += user.Submarine.Position; + } + } + else + { + worldPos = entity.WorldPosition; + } switch (chosenItemSpawnInfo.RotationType) { case ItemSpawnInfo.SpawnRotationType.Fixed: @@ -1731,7 +1802,7 @@ namespace Barotrauma } break; case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.SimPosition - spawnPos); + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); break; case ItemSpawnInfo.SpawnRotationType.Limb: if (sourceBody != null) @@ -1775,6 +1846,15 @@ namespace Barotrauma rotation += spread; if (projectile != null) { + Vector2 spawnPos; + if (projectile.Hitscan) + { + spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; + } + else + { + spawnPos = ConvertUnits.ToSimUnits(worldPos); + } projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); } @@ -1797,9 +1877,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; } @@ -1883,6 +1969,10 @@ namespace Barotrauma ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); intervalTimer = Interval; + if (oneShot) + { + Disabled = true; + } static Character CharacterFromTarget(ISerializableEntity target) { @@ -2055,12 +2145,13 @@ namespace Barotrauma if (entity is Item sourceItem && sourceItem.HasTag("medical")) { multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user != null) + + if (user is not null) { multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); } } + return multiplier * AfflictionMultiplier; } @@ -2072,6 +2163,18 @@ 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") + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); + } + } + if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); @@ -2088,7 +2191,7 @@ namespace Barotrauma if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { - if (type == ActionType.OnUse) + if (type == ActionType.OnUse || type == ActionType.OnSuccess) { limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index b4aa8a883..d83ebf7af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -310,7 +310,7 @@ namespace Barotrauma.Steam + "unexpected deletion of your hard work.\n" + "Instead, modify a copy of your mod in LocalMods.\n"; - string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt"); + string workshopModDirReadmeLocation = Path.Combine(SaveUtil.DefaultSaveFolder, "WorkshopMods", "README.txt"); if (!File.Exists(workshopModDirReadmeLocation)) { Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs index 7f27612ea..6ddc13c18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -13,7 +13,7 @@ namespace Barotrauma Yes } - protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None; + public LanguageIdentifier Language { get; private set; } = LanguageIdentifier.None; private int languageVersion = 0; protected string cachedValue = ""; @@ -32,13 +32,13 @@ namespace Barotrauma protected void UpdateLanguage() { - language = GameSettings.CurrentConfig.Language; + Language = GameSettings.CurrentConfig.Language; languageVersion = TextManager.LanguageVersion; } protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes { - return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; + return Language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; } protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 0056fc9da..7c8ede811 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -54,10 +54,10 @@ namespace Barotrauma return (loaded ? candidates.GetRandomUnsynced() : "", loaded); } - var (value, loaded) = tryLoad(language); + var (value, loaded) = tryLoad(Language); loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No; cachedValue = value; - if (!loaded && language != TextManager.DefaultLanguage) + if (!loaded && Language != TextManager.DefaultLanguage) { (value, _) = tryLoad(TextManager.DefaultLanguage); cachedValue = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 78cc321f7..f4bd75f05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma public static class TextManager { + public static bool DebugDraw; + public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); public static IEnumerable AvailableLanguages => TextPacks.Keys; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 095b1bd38..1dca1dad8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -43,15 +43,28 @@ 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(); if (level > maxLevel) { maxLevel = level; } - int price = BasePrice; - price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100); - return location?.GetAdjustedMechanicalCost(price) ?? price; + float price = BasePrice; + price += price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100f; + price = location?.GetAdjustedMechanicalCost((int)price) ?? price; + + characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both); + + if (characterList.Any()) + { + if (location?.Reputation is { } reputation && Faction.GetPlayerAffiliationStatus(reputation.Identifier, characterList) 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; } } @@ -108,6 +121,8 @@ namespace Barotrauma public readonly bool IsWallUpgrade; public readonly LocalizedString Name; + private readonly object mutex = new object(); + public readonly IEnumerable ItemTags; public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) @@ -119,7 +134,6 @@ namespace Barotrauma ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); - if (!nameIdentifier.IsEmpty) { Name = TextManager.Get($"{nameIdentifier}"); @@ -132,10 +146,13 @@ namespace Barotrauma public void DeterminePrefabsThatAllowUpgrades() { - prefabsThatAllowUpgrades.Clear(); - prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs - .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) - .Select(it => it.Identifier)); + lock (mutex) + { + prefabsThatAllowUpgrades.Clear(); + prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs + .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) + .Select(it => it.Identifier)); + } } public bool CanBeApplied(MapEntity item, UpgradePrefab? upgradePrefab) @@ -153,26 +170,11 @@ namespace Barotrauma if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); - } - - public bool CanBeApplied(XElement element, UpgradePrefab prefab) - { - if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } - - Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); - if (identifier.IsEmpty) { return false; } - - ItemPrefab? item = ItemPrefab.Find(null, identifier); - if (item == null) { return false; } - - Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); - - if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } - - return item.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); + lock (mutex) + { + return item.Prefab.GetAllowedUpgrades().Contains(Identifier) || + ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); + } } public static UpgradeCategory? Find(Identifier identifier) @@ -209,9 +211,17 @@ namespace Barotrauma { 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); + + subTier = Math.Max(modifier, subTier); + } + if (tierOrClass.TryGet(out int tier)) { - return sub.Tier == tier; + return subTier == tier; } if (tierOrClass.TryGet(out SubmarineClass subClass)) @@ -443,6 +453,12 @@ namespace Barotrauma if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) + { + int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); + level += modifier; + } + return level; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 688c284c5..0d6974172 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -104,6 +104,8 @@ namespace Barotrauma (float)Math.Floor(value / div) * div; } + public static int RoundToInt(float v) => (int)MathF.Round(v); + public static float RoundTowardsClosest(float value, float div) { return (float)Math.Round(value / div) * div; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs new file mode 100644 index 000000000..7467600c5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NetCollection.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma +{ + [NetworkSerialize] + public readonly record struct NetCollection(ImmutableArray Array) : INetSerializableStruct, IEnumerable + { + public static readonly NetCollection Empty = new(ImmutableArray.Empty); + + public NetCollection(params T[] elements) : this(elements.ToImmutableArray()) { } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 8df000e58..55120383d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -18,7 +18,7 @@ namespace Barotrauma #if OSX //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac - public static readonly string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", @@ -27,13 +27,13 @@ namespace Barotrauma #else //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux - public static readonly string SaveFolder = Path.Combine( + public static readonly string DefaultSaveFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Daedalic Entertainment GmbH", "Barotrauma"); #endif - public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer"); + public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer"); public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded"); @@ -43,9 +43,9 @@ namespace Barotrauma public static string TempPath { #if SERVER - get { return Path.Combine(SaveFolder, "temp_server"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); } #else - get { return Path.Combine(SaveFolder, "temp"); } + get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); } #endif } @@ -198,7 +198,10 @@ namespace Barotrauma } //deleting a multiplayer save file -> also delete character data - if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder))) + var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath)); + + if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) || + fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer))) { string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath); if (File.Exists(characterDataSavePath)) @@ -215,35 +218,70 @@ namespace Barotrauma } } - public static string GetSavePath(SaveType saveType, string saveName) + public static string GetSaveFolder(SaveType saveType) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - return Path.Combine(folder, saveName); + string folder = string.Empty; + + if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath)) + { + folder = GameSettings.CurrentConfig.SavePath; + if (saveType == SaveType.Multiplayer) + { + folder = Path.Combine(folder, "Multiplayer"); + } + if (!Directory.Exists(folder)) + { + DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder..."); + try + { + Directory.CreateDirectory(folder); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e); + folder = string.Empty; + } + } + } + if (string.IsNullOrEmpty(folder)) + { + folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + } + return folder; } public static IReadOnlyList GetSaveFiles(SaveType saveType, bool includeInCompatible = true) { - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; - if (!Directory.Exists(folder)) + string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder; + if (!Directory.Exists(defaultFolder)) { - DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder..."); + DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder..."); try { - Directory.CreateDirectory(folder); + Directory.CreateDirectory(defaultFolder); } catch (Exception e) { - DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e); + DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e); } } - List files = Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + List files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList(); + + var folder = GetSaveFolder(saveType); + if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) + { + files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); + } + string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder; if (Directory.Exists(legacyFolder)) { files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); } + files = files.Distinct().ToList(); + List saveInfos = new List(); foreach (string file in files) { @@ -305,7 +343,7 @@ namespace Barotrauma { fileName = ToolBox.RemoveInvalidFileNameChars(fileName); - string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; + string folder = GetSaveFolder(saveType); if (fileName == "Save_Default") { fileName = TextManager.Get("SaveFile.DefaultName").Value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 1f5b69a16..dba2f7ea7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; @@ -101,7 +102,7 @@ namespace Barotrauma string startPath = directory ?? ""; - string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/'); + string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/'); if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { //paths that lead to the save folder might have incorrect case, @@ -721,5 +722,119 @@ namespace Barotrauma { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; } + + private static readonly ImmutableHashSet affectedCharacters = ImmutableHashSet.Create('%', '+', '%'); + + /// + /// Extends % and + characters to color tags in talent name tooltips to make them look nicer. + /// This obviously does not work in languages like French where a non breaking space is used + /// so it's just a a bit extra for the languages it works with. + /// + /// + /// + public static string ExtendColorToPercentageSigns(string original) + { + const string colorEnd = "‖color:end‖", + colorStart = "‖color:"; + + const char definitionIndicator = '‖'; + + char[] chars = original.ToCharArray(); + + for (int i = 0; i < chars.Length; i++) + { + if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; } + + // look behind + if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator) + { + int offset = colorEnd.Length; + + if (MatchesSequence(i - offset, colorEnd, chars)) + { + // push the color end tag forwards until the character is within the tag + char prev = currentChar; + for (int k = i - offset; k <= i; k++) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + continue; + } + } + + // look ahead + if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator) + { + if (!MatchesSequence(i + 1, colorStart, chars)) { continue; } + + int offset = FindNextDefinitionOffset(i, colorStart.Length, chars); + + // we probably reached the end of the string + if (offset > chars.Length) { continue; } + + // push the color start tag back until the character is within the tag + char prev = currentChar; + for (int k = i + offset; k >= i; k--) + { + if (!TryGetAt(k, chars, out c)) { continue; } + + chars[k] = prev; + prev = c; + } + + // skip needlessly checking this section again since we already know what's ahead + i += offset; + } + } + + static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars) + { + int offset = initialOffset; + while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; } + return offset; + } + + static bool MatchesSequence(int index, string sequence, char[] chars) + { + for (int i = 0; i < sequence.Length; i++) + { + if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; } + } + + return true; + } + + static bool TryGetAt(int i, char[] chars, out char c) + { + if (i >= 0 && i < chars.Length) + { + c = chars[i]; + return true; + } + + c = default; + return false; + } + + return new string(chars); + } + + public static bool StatIdentifierMatches(Identifier original, Identifier match) + { + if (original == match) { return true; } + + for (int i = 0; i < match.Value.Length; i++) + { + if (i >= original.Value.Length) { return match[i] is '~'; } + if (!CharEquals(original[i], match[i])) { return false; } + } + + return false; + + static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b); + } } } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 3c57a94c3..5545c4f44 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,276 @@ +--------------------------------------------------------------------------------------------------------- +v100.4.0.0 +--------------------------------------------------------------------------------------------------------- + +- Updated loading screen and main menu graphics. +- Hid some of the endgame items from the sub editor. +- Fixed research beacon station sometimes spawning in normal levels. +- Fixed research beacon station's generator not generating any power. +- Fixed end boss sometimes spawning a bit off from the intended spawn position. + +--------------------------------------------------------------------------------------------------------- +v100.3.0.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed crashing when you enter the shipyard interface. + +--------------------------------------------------------------------------------------------------------- +v100.2.0.0 +--------------------------------------------------------------------------------------------------------- + +- Added a new ending for the campaign. +- New loading screen / location portraits. + +Fixes: +- Fixed cultist robe's hood rendering over diving suit helmets. +- Fixed colonies sometimes failing to generate properly (falling back to a small pre-made outpost). +- Fixed some of the hireable characters spawning without a headset. +- Made honker blast require and consume fuel to prevent it from being infinitely spammable. +- Fixed NPCs not attacking you in the Subra rescue mission. +- Fixed "no mission selected" popup being shown if there's no missions available for the next round, but an outpost mission available in the destination. +- Fixed mining mission sonar labels displaying as [resourcename]. + +--------------------------------------------------------------------------------------------------------- +v100.1.0.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed all colonies having clown deco in them. +- Made Funbringer 3000 craftable. +- Fixed Clownhaven mission sometimes unlocking in non-colony outposts. +- Fixed texts duplicating after each round in the Book of Chalices and Honkmotherian Scriptures. +- Fixed bots fleeing from the huskified storage container. +- Fixed console error about "missionevent_escort1" when entering the 2nd outpost. + +--------------------------------------------------------------------------------------------------------- +v100.0.0.0 +--------------------------------------------------------------------------------------------------------- + +Test version of the 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. +- Lots of new outpost events, and a longer "event chain" for the secondary factions. +- Lots of new facion-specific missions: some variants of existing missions, some new. +- 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). +- 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 Separatists. +- Improvements to the campaign map. + +--------------------------------------------------------------------------------------------------------- +v0.20.6.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed scripted events looping and repeating some parts. +- Depth charges don't explode when taking damage until they've been launched. +- Fixed Acid Grenade quality doing nothing. +- Fixed Acid Grenades not working in detonators. +- Fixed genetic materials being too abundant with the "Blackmarket Genes" talent. +- Fixed affliction icons overlapping with the inventory when grabbing someone or when a health interface is open. +- Fixed scrap cannon's firing particle effect triggering without ammo. +- Fixed scrap cannon barrel position. +- Fixed messed up depleted fuel SMG magazine sprite. + +Bugfixes: +- Fixed portable pump's per-sub limit not working if you attach them at a spot with no background wall. +- Fixed oxygenlow resistance not affecting the time it takes to die in an unconscious state. +- Fixed bloodloss resistance not affecting how fast bleeding causes bloodloss. +- Fixed numpad keys toggling the chat when the Chat key is bind to nothing. +- Fixed piezo crystals no longer spawning in the Great Sea. + +Modding: +- Fixed sound's frequency multiplier not working in many cases (status effects, specific item sounds like turret movement sounds). + +--------------------------------------------------------------------------------------------------------- +v0.20.5.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Fixed crashing when you enter the 2nd outpost. + +--------------------------------------------------------------------------------------------------------- +v0.20.4.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added an objective to give the order to "Operate Turret" alongside other orders in the campaign tutorial. +- Added popups when completing tutorial chapters that allow you to restart or continue and to return back to the menu. +- Rebalanced clothing and apparel resistances. Overall basic clothes will give less resistance. +- Rebalanced damage dealt by tools. Damage should be a bit higher overall. +- SMG can now be crafted. +- Items' damage modifiers are shown in store tooltips. +- Added an option to hide enemy health bars. +- Server visibility can be adjusted in the server lobby (instead of having to restart the server). +- When throwing an item (such as a grenade), the whole throw animation is played before the item is actually launched. Prevents being able to throw items at a ridiculous rate by spamming the hotkeys and LMB. +- Players can select talents for bots in multiplayer. +- Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites). + +Balance: +- Made some weapons available later in game, to increase feeling of progression. + +Unstable only: +- Fixed campaign tutorial progress resetting. +- Fixed tutorial mudraptors spawning in the wrong places. +- Fixed a Role tutorial cabinet staying highlighted after completing the required action. +- Fixed fabricator taking items from linked containers even if there's suitable ones in the input slots. +- Fixed typos in europabrew and sulphuric acid syringes that made them craftable by anyone. +- Fixed several talents that were not working as described. +- Various balance changes to talents. +- Fixed an exploit that allowed making sonar pings undetectable by turning the sonar off after the ping. +- Fixed swapping magazines no longer working due to the addition of the flashlight slots. +- Fixed bots being unable to swap Exosuit's oxygen tanks or use it as divining gear. +- Fixed rifle bullets appearing at the tip of the rifle. +- Fixed limbs sometimes getting severed off mudraptors or other monsters when the target is alive, leading to floating mudraptors. + +Fixed: +- Fixed candidate box not being visible when using the Chinese input method. +- Fixed switching characters interrupting outpost events (even if there's currently no dialog active). +- Fixed certain events preventing other events from triggering when half-finished (e.g. preventing you from unlocking missions when an event is still running and for example waiting for you to talk to some other NPC). +- Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer. +- Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader. + +--------------------------------------------------------------------------------------------------------- +v0.20.3.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle. +- Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost. +- Added in-game hints for the genetic system. +- Added a button for treating all characters in one go to the medical clinic. +- Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself. + +Unstable only: +- Various balance changes to talents. +- Fixed clear button not doing anythign in new talent menu. +- Fixed various XP-giving talents causing console errors at the end of the round. +- Rum can be drunk (not just applied in the health interface). +- Fixed defense bot not aiming properly client-side. +- Fixed some talent items being craftable without the talent. +- Fixed some talent items being sold in outpost stores. +- Replaced some placeholder talent item sprites. +- Fixed "Mailman" talent multiplying mission rewards by 151. +- Fixed container indicator showing how full the 1st slot is when it should show how full the container is. +- Fixed crashing if a fabricator finishes fabricating with no user. +- Fixed skedaddle talent only giving 1 second of the buff instead of 5. +- Fixed clown crate not fit in crate shelf. + +Balance: +- Slightly adjusted values of handheld weapons. + - Power levels match cost better. + - Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls). + - Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though). + - Improved ammo availability for basic weapons. +- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses. + - Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances. + - PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths. + - Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair. + - All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit. +- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Plasma cutter is now much better at cutting. + +Bugfixes: +- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor". + +Modding: +- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses). +- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration). +- Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body). +- Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill. +- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. F.e. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s. + +--------------------------------------------------------------------------------------------------------- +v0.20.2.0 +--------------------------------------------------------------------------------------------------------- + +Unstable only: +- Balance improvements to the new talents. +- Fixed mistakes and inconsistencies in talent descriptions. +- Fixed several talents reducing item stats instead of increasing them. +- Fixed console errors caused by the Bounty Hunter talent. +- Fixed Bloody Business not working if you have the Gene Harvester talent unlocked. +- Added a proper sprite for the exosuit. +- Fixed exosuit working after you take out the battery. +- Fixed exosuit working regardless of which slot you put the battery and oxygen tank in. +- Fixed arc emitter sprite when worn on back. +- Fixed console error when hitting a shield with a melee weapon. +- Reduced makeshift shelves to 2 slots to match sprite. +- Reduced amount of talents available through Endocrine Booster usage. +- Add missing recipe unlocks: Ceremonial Sword, Handcannon. +- Moved "Steady Tune" to tier 2, added a new "Trickle Down" talent to the 3rd tier. + +--------------------------------------------------------------------------------------------------------- +v0.20.1.0 +--------------------------------------------------------------------------------------------------------- + +Tutorial improvements: +- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. 1st version: feedback and issue reports are much appreciated! +- Various fixes and improvements to the Basic and Role tutorials. + +Unstable only: +- Made saline a bit more effective (the nerf in the previous unstable version was a bit too much). +- Fixed ranged creature attacks causing a crash when the creature is targeting a hull. +- Fixed monsters with no inventory (e.g. watcher, hammerhead spawns) causing a crash. +- Most of the talent fixes and improvements we've been doing didn't make it into this build, but they're coming soon! + +--------------------------------------------------------------------------------------------------------- +v0.20.0.0 +--------------------------------------------------------------------------------------------------------- + +Talent overhaul: +- Redesigned and rebalanced talents: lots of new talents, changes and balancing to existing ones and replacing some of the more broken ones with new ones. +- Redesigned talent tree structure: there's now a selection of "generic" talents, and after you've unlocked 4 of them, you can choose one specialization talent tree (meaning that you can no longer unlock all trees, but have to specialize in one). +- There's still a lot to test, balance and fix in the new talents, so all feedback and issue reports regarding the talents are much appreciated: + - Are there some talents that don't work as expected? + - Are the XP bonuses too large or too small? + - In general, how balanced are the talents, are there any that feel OP or too weak? + - Any inaccuracies or things to improve in the talent descriptions? + +Changes and additions: +- Added a button for opening the Steam Workshop to all tabs of the workshop menu. +- Added tooltips that explain how the bot spawn modes work to the server lobby. +- Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful. +- Nerfed flak cannon's explosive ammo. +- Allow putting medium items (e.g. storage container) in medical and toxic cabinets. + +Multiplayer: +- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself. +- Fixed networking errors when the connection to the server is momentarily lost and then re-established. +- Added a cooldown to client name changes to prevent using it for spamming. +- Fixed bans issued with the "banaddress" command using a client's Steam ID not working. + +Bugfixes: +- Fixed Esc not closing the campaign interfaces (map, store, shipyard, etc) but opening the pause menu instead. +- Fixed bots sometimes firing in a random direction when they equip a weapon. Happened because the aim and shoot inputs could already be active when the bot switches from another item to the weapon (e.g. from underwater scooter to some gun). +- Fixed pirates sometimes being unable to operate multiple turrets at the same time (even if there's enough crew to operate multiple), and attempting to operate hardpoints. +- Fabricator chooses the available ingredient that's in the worst condition when there's multiple suitable ingredients available. +- Fixed characters falling off ladders when using aimable tools. +- Fixed currently selected mission being included in the mission count displayed on the campaign map (i.e. showing "1/2" when you're choosing a new destination at an empty location). +- Fixed inability to sit in cafeteria chairs. +- Fixed projectile impacts getting triggered by gravity spheres and other TriggerComponents (meaning you couldn't hit monsters near a gravity sphere). +- Fixed blood pack fabrication recipe outputting only one item. +- Fixed tutorial not progressing when inserting a welding fuel tank inside the welding tool straight away, rather than inventory first. +- Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it. +- Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now. +- Fixed items getting autofilled into non-interactable containers in wrecks and outposts. +- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses). +- Fixed verifying file integrity on Steam resetting the server settings file. +- Fixed crashing if you try to open an access-restricted directory in the file selection dialog. +- Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage. +- Fixed money gain/lose popups no longer showing in the campaign. +- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects. +- Fixed bots always opening the door/hatch they're trying to repair. +- Fixed power indicator not rotating with batteries. +- Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them. +- Fixed Camel's airlock not draining fully. +- Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor. + +Modding: +- Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged). +- Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character. +- Fixed crashing if the ingredient of a fabrication recipe can't be found. +- Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple). --------------------------------------------------------------------------------------------------------- v0.19.14.0 @@ -75,6 +348,30 @@ Fixes to new issues in the previous build: - Fixed clients sometimes failing to reconnect to the server if the connection is momentarily lost. - Fixed item conditions resetting to 100% between rounds. - Fixed mineral missions always causing a crash. +feature/combat-n-handhelds-balance branch changes +--------------------------------------------------------------------------------------------------------- + +Balance: +- Slightly adjusted values of handheld weapons +-- Power levels match cost better +-- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls) +-- Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though) +-- Improved ammo availability for basic weapons +- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses +-- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances +-- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths +-- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair +-- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit +- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes. +- Plasma cutter is now much better at cutting + +Modding: +- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses) +- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration) +- Added 'ApplyToHands' property to Propulsion (default=true), preventing extra force applying to hands when the item is held in hands when set to false (instead applying only to the character's whole body) +- Added a skill requirement conditional for StatusEffect, example: to make a status effect occur only if the target has less than 35 weapon skill +- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. F.e. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5s + --------------------------------------------------------------------------------------------------------- v0.19.8.0 @@ -404,7 +701,8 @@ Changes: - Handheld sonars can't detect minerals from inside the sub. - Changed the plus and minus button in the campaign settings into arrows. The button on the right increases difficulty, which in the case of the starting balance and supplies means reducing them, making the plus and minus buttons misleading. - Reduced costs of handheld weapon ammunition significantly. -- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper ammo. +- Up revolver & harpoon shop availability (especially at military outposts / armory merchants) +- Slightly reduced effectiveness of harpoons and revolver round to compensate for the cheaper / more available ammo. - Changed recipes for Handcannon, Assault Rifle and Auto-Shotgun. Weapon crafting is more expensive, to compensate for cheaper ammo. - Adjusted numerous other recipes and price costs of materials. Previously little used materials (like tin) are now used more. - Partially reintroduced the "toggle inventory" keybind, now called "toggle entity list". Even though toggling the in-game inventory is no longer possible, the keybind can be used to change the hotkey for toggling the sub editor's entity list. diff --git a/Barotrauma/BarotraumaShared/hintmanager.xml b/Barotrauma/BarotraumaShared/hintmanager.xml index ed6c346cc..3f843afaa 100644 --- a/Barotrauma/BarotraumaShared/hintmanager.xml +++ b/Barotrauma/BarotraumaShared/hintmanager.xml @@ -58,6 +58,11 @@ + + + + + diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml deleted file mode 100644 index 482899fc2..000000000 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ /dev/null @@ -1,57 +0,0 @@ - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs new file mode 100644 index 000000000..305187f05 --- /dev/null +++ b/Barotrauma/BarotraumaTest/GenericToolBoxTests.cs @@ -0,0 +1,55 @@ +#nullable enable + +using System; +using Xunit; +using Barotrauma; +using FluentAssertions; +using FsCheck; + +namespace TestProject; + +public sealed class GenericToolBoxTests +{ + public class CustomGenerators + { + public static Arbitrary IdentifierPairGenerator() + { + return Arb.From(from Identifier first in Arb.Generate() + from Identifier second in Arb.Generate().Where(second => second != first) + select new DifferentIdentifierPair(first, second)); + } + } + + public readonly struct DifferentIdentifierPair + { + public readonly Identifier First, + Second; + + public DifferentIdentifierPair(Identifier first, Identifier second) + { + if (first == second) { throw new InvalidOperationException("Identifiers must be different"); } + + First = first; + Second = second; + } + } + + public GenericToolBoxTests() + { + Arb.Register(); + Arb.Register(); + } + + [Fact] + public void MatchesStatIdentifier() + { + Prop.ForAll(static pair => + { + ToolBox.StatIdentifierMatches(pair.First, $"{pair.First}~{pair.Second}".ToIdentifier()).Should().BeTrue(); + ToolBox.StatIdentifierMatches(pair.First, pair.First).Should().BeTrue(); + + ToolBox.StatIdentifierMatches(pair.First, $"{pair.Second}~{pair.First}".ToIdentifier()).Should().BeFalse(); + ToolBox.StatIdentifierMatches(pair.First, pair.Second).Should().BeFalse(); + }).VerboseCheckThrowOnFailure(); + } +} diff --git a/Barotrauma/BarotraumaTest/TestProject.cs b/Barotrauma/BarotraumaTest/TestProject.cs index 614b0b23b..6b36c44e1 100644 --- a/Barotrauma/BarotraumaTest/TestProject.cs +++ b/Barotrauma/BarotraumaTest/TestProject.cs @@ -15,6 +15,12 @@ namespace TestProject select new Vector2(x, y)); } + public static Arbitrary IdentifierGenerator() + { + return Arb.From(from string value in Arb.Generate().Where(static s => s != null) + select new Identifier(value)); + } + public static Arbitrary ColorGenerator() { return Arb.From(from int r in Gen.Choose(0, 255) diff --git a/Libraries/ImeSharp/IMEString.cs b/Libraries/ImeSharp/IMEString.cs new file mode 100644 index 000000000..6635c85ae --- /dev/null +++ b/Libraries/ImeSharp/IMEString.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ImeSharp +{ + public unsafe struct IMEString : IEnumerable + { + internal const int IMECharBufferSize = 64; + + public static readonly IMEString Empty = new IMEString((List)null); + + internal struct Enumerator : IEnumerator + { + private IMEString _imeString; + private char _currentCharacter; + private int _currentIndex; + + public Enumerator(IMEString imeString) + { + _imeString = imeString; + _currentCharacter = '\0'; + _currentIndex = -1; + } + + public bool MoveNext() + { + int size = _imeString.Count; + + _currentIndex++; + + if (_currentIndex == size) + return false; + + fixed (char* ptr = _imeString.buffer) + { + _currentCharacter = *(ptr + _currentIndex); + } + + return true; + } + + public void Reset() + { + _currentIndex = -1; + } + + public void Dispose() + { + } + + public char Current { get { return _currentCharacter; } } + object IEnumerator.Current { get { return Current; } } + } + + public int Count { get { return _size; } } + + public char this[int index] + { + get + { + if (index >= Count || index < 0) + throw new ArgumentOutOfRangeException("index"); + + fixed (char* ptr = buffer) + { + return *(ptr + index); + } + } + } + + private int _size; + + fixed char buffer[IMECharBufferSize]; + + public IMEString(string characters) + { + if (string.IsNullOrEmpty(characters)) + { + _size = 0; + return; + } + + _size = characters.Length; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(List characters) + { + if (characters == null || characters.Count == 0) + { + _size = 0; + return; + } + + _size = characters.Count; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(char[] characters, int count) + { + if (characters == null || count <= 0) + { + _size = 0; + return; + } + + _size = count; + if (_size > IMECharBufferSize) + _size = IMECharBufferSize - 1; + + if (_size > characters.Length) + _size = characters.Length; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + for (var i = 0; i < _size; i++) + { + *ptr = characters[i]; + ptr++; + } + } + } + + public IMEString(IntPtr bStrPtr) + { + if (bStrPtr == IntPtr.Zero) + { + _size = 0; + return; + } + + var ptrSrc = (char*)bStrPtr; + + int i = 0; + + fixed (char* _ptr = buffer) + { + char* ptr = _ptr; + + while (ptrSrc[i] != '\0') + { + *ptr = ptrSrc[i]; + i++; + ptr++; + } + } + + _size = i; + } + + public override string ToString() + { + fixed (char* ptr = buffer) + { + return new string(ptr, 0, _size); + } + } + + public IntPtr ToIntPtr() + { + fixed (char* ptr = buffer) + { + return (IntPtr)ptr; + } + } + + public IEnumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Libraries/ImeSharp/IMETextCompositionEventArgs.cs b/Libraries/ImeSharp/IMETextCompositionEventArgs.cs new file mode 100644 index 000000000..93548b0be --- /dev/null +++ b/Libraries/ImeSharp/IMETextCompositionEventArgs.cs @@ -0,0 +1,61 @@ +using System; + +namespace ImeSharp +{ + /// + /// Arguments for the event. + /// + public struct IMETextCompositionEventArgs + { + /// + // Construct a TextCompositionEventArgs with composition infos. + /// + public IMETextCompositionEventArgs(IMEString compositionText, + int cursorPosition, + IMEString[] candidateList = null, + int candidatePageStart = 0, + int candidatePageSize = 0, + int candidateSelection = 0) + { + CompositionText = compositionText; + CursorPosition = cursorPosition; + + CandidateList = candidateList; + CandidatePageStart = candidatePageStart; + CandidatePageSize = candidatePageSize; + CandidateSelection = candidateSelection; + } + + /// + /// The full string as it's composed by the IMM. + /// + public readonly IMEString CompositionText; + + /// + /// The position of the cursor inside the composed string. + /// + public readonly int CursorPosition; + + /// + /// The candidate text list for the composition. + /// This property is only supported on WindowsDX and WindowsUniversal. + /// If the composition string does not generate candidates this array is empty. + /// + public readonly IMEString[] CandidateList; + + /// + /// First candidate index of current page. + /// + public readonly int CandidatePageStart; + + /// + /// How many candidates should display per page. + /// + public readonly int CandidatePageSize; + + /// + /// The selected candidate index. + /// + public readonly int CandidateSelection; + } +} diff --git a/Libraries/ImeSharp/IMETextInputEventArgs.cs b/Libraries/ImeSharp/IMETextInputEventArgs.cs new file mode 100644 index 000000000..2d415a21e --- /dev/null +++ b/Libraries/ImeSharp/IMETextInputEventArgs.cs @@ -0,0 +1,12 @@ +namespace ImeSharp +{ + public struct IMETextInputEventArgs + { + public IMETextInputEventArgs(char character) + { + Character = character; + } + + public readonly char Character; + } +} \ No newline at end of file diff --git a/Libraries/ImeSharp/ImeSharp.csproj b/Libraries/ImeSharp/ImeSharp.csproj new file mode 100644 index 000000000..8cd198465 --- /dev/null +++ b/Libraries/ImeSharp/ImeSharp.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + true + + + + C# wrapper for Windows IME APIs. Its goal is to support both IMM32 and TSF. + IME;netcoreapp3.1;net5.0;net6.0;winforms;windows;tsf;imm32 + ImeSharp + https://github.com/ryancheung/ImeSharp + https://github.com/ryancheung/ImeSharp + ryancheung + MIT + + + + ..\Artifacts + ImeSharp + ImeSharp + 5 + + + + + + + + diff --git a/Libraries/ImeSharp/Imm32Manager.cs b/Libraries/ImeSharp/Imm32Manager.cs new file mode 100644 index 000000000..9cf0c2768 --- /dev/null +++ b/Libraries/ImeSharp/Imm32Manager.cs @@ -0,0 +1,349 @@ +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Globalization; +using System.Diagnostics; +using ImeSharp.Native; + +namespace ImeSharp +{ + internal class Imm32Manager + { + + // If the system is IMM enabled, this is true. + private static bool _immEnabled = SafeSystemMetrics.IsImmEnabled; + + public static bool ImmEnabled { get { return _immEnabled; } } + + public const int LANG_CHINESE = 0x04; + public const int LANG_KOREAN = 0x12; + public const int LANG_JAPANESE = 0x11; + + public static int PRIMARYLANGID(int lgid) + { + return ((ushort)(lgid) & 0x3ff); + } + + static Imm32Manager() + { + SetCurrentCulture(); + } + + /// + /// return true if the current keyboard layout is a real IMM32-IME. + /// + public static bool IsImm32ImeCurrent() + { + if (!_immEnabled) + return false; + + IntPtr hkl = NativeMethods.GetKeyboardLayout(0); + + return IsImm32Ime(hkl); + } + + /// + /// return true if the keyboard layout is a real IMM32-IME. + /// + public static bool IsImm32Ime(IntPtr hkl) + { + if (hkl == IntPtr.Zero) + return false; + + return ((NativeMethods.IntPtrToInt32(hkl) & 0xf0000000) == 0xe0000000); + } + + private static int _inputLanguageId; + + internal static void SetCurrentCulture() + { + var hkl = NativeMethods.GetKeyboardLayout(0); + _inputLanguageId = NativeMethods.IntPtrToInt32(hkl) & 0xFFFF; + } + + private IntPtr _windowHandle; + + private IntPtr _defaultImc; + private IntPtr DefaultImc + { + get + { + if (_defaultImc == IntPtr.Zero) + { + IntPtr himc = NativeMethods.ImmCreateContext(); + + // Store the default imc to _defaultImc. + _defaultImc = himc; + } + return _defaultImc; + } + } + + private static ImmCompositionStringHandler _compositionStringHandler; + private static ImmCompositionIntHandler _compositionCursorHandler; + + private bool _lastImmOpenStatus; + + public Imm32Manager(IntPtr windowHandle) + { + _windowHandle = windowHandle; + + _compositionStringHandler = new ImmCompositionStringHandler(DefaultImc, NativeMethods.GCS_COMPSTR); + _compositionCursorHandler = new ImmCompositionIntHandler(DefaultImc, NativeMethods.GCS_CURSORPOS); + } + + public static Imm32Manager Current + { + get + { + var defaultImm32Manager = InputMethod.DefaultImm32Manager; + + if (defaultImm32Manager == null) + { + defaultImm32Manager = new Imm32Manager(InputMethod.WindowHandle); + InputMethod.DefaultImm32Manager = defaultImm32Manager; + } + + return defaultImm32Manager; + } + } + + public void Enable() + { + if (DefaultImc != IntPtr.Zero) + { + // Create a temporary system caret + NativeMethods.CreateCaret(_windowHandle, IntPtr.Zero, 2, 10); + NativeMethods.ImmAssociateContext(_windowHandle, _defaultImc); + } + } + + public void Disable() + { + NativeMethods.ImmAssociateContext(_windowHandle, IntPtr.Zero); + NativeMethods.DestroyCaret(); + } + + const int kCaretMargin = 1; + + // Set candidate window position. + // Borrowed from https://github.com/chromium/chromium/blob/master/ui/base/ime/win/imm32_manager.cc + public void SetCandidateWindow(TsfSharp.Rect caretRect) + { + int x = caretRect.Left; + int y = caretRect.Top; + + if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE) + { + // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow() + // when a user disables TSF (Text Service Framework) and CUAS (Cicero + // Unaware Application Support). + // On the other hand, when a user enables TSF and CUAS, Chinese IMEs + // ignore the position of the current system caret and uses the + // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle' + // parameter CFS_CANDIDATEPOS. + // Therefore, we do not only call ::ImmSetCandidateWindow() but also + // set the positions of the temporary system caret. + var candidateForm = new NativeMethods.CANDIDATEFORM(); + candidateForm.dwStyle = NativeMethods.CFS_CANDIDATEPOS; + candidateForm.ptCurrentPos.X = x; + candidateForm.ptCurrentPos.Y = y; + NativeMethods.ImmSetCandidateWindow(_defaultImc, ref candidateForm); + } + + if (PRIMARYLANGID(_inputLanguageId) == LANG_JAPANESE) + NativeMethods.SetCaretPos(x, caretRect.Bottom); + else + NativeMethods.SetCaretPos(x, y); + + // Set composition window position also to ensure move the candidate window position. + var compositionForm = new NativeMethods.COMPOSITIONFORM(); + compositionForm.dwStyle = NativeMethods.CFS_POINT; + compositionForm.ptCurrentPos.X = x; + compositionForm.ptCurrentPos.Y = y; + NativeMethods.ImmSetCompositionWindow(_defaultImc, ref compositionForm); + + if (PRIMARYLANGID(_inputLanguageId) == LANG_KOREAN) + { + // Chinese IMEs and Japanese IMEs require the upper-left corner of + // the caret to move the position of their candidate windows. + // On the other hand, Korean IMEs require the lower-left corner of the + // caret to move their candidate windows. + y += kCaretMargin; + } + + // Need to return here since some Chinese IMEs would stuck if set + // candidate window position with CFS_EXCLUDE style. + if (PRIMARYLANGID(_inputLanguageId) == LANG_CHINESE) return; + + // Japanese IMEs and Korean IMEs also use the rectangle given to + // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE + // to move their candidate windows when a user disables TSF and CUAS. + // Therefore, we also set this parameter here. + var excludeRectangle = new NativeMethods.CANDIDATEFORM(); + compositionForm.dwStyle = NativeMethods.CFS_EXCLUDE; + compositionForm.ptCurrentPos.X = x; + compositionForm.ptCurrentPos.Y = y; + compositionForm.rcArea.Left = x; + compositionForm.rcArea.Top = y; + compositionForm.rcArea.Right = caretRect.Right; + compositionForm.rcArea.Bottom = caretRect.Bottom; + NativeMethods.ImmSetCandidateWindow(_defaultImc, ref excludeRectangle); + } + + internal bool ProcessMessage(IntPtr hWnd, uint msg, ref IntPtr wParam, ref IntPtr lParam) + { + switch (msg) + { + case NativeMethods.WM_INPUTLANGCHANGE: + SetCurrentCulture(); + break; + case NativeMethods.WM_IME_SETCONTEXT: + if (wParam.ToInt32() == 1 && InputMethod.Enabled) + { + // Must re-associate ime context or things won't work. + NativeMethods.ImmAssociateContext(_windowHandle, DefaultImc); + + if (_lastImmOpenStatus) + NativeMethods.ImmSetOpenStatus(DefaultImc, true); + + var lParam64 = lParam.ToInt64(); + if (!InputMethod.ShowOSImeWindow) + lParam64 &= ~NativeMethods.ISC_SHOWUICANDIDATEWINDOW; + else + lParam64 &= ~NativeMethods.ISC_SHOWUICOMPOSITIONWINDOW; + lParam = (IntPtr)(int)lParam64; + } + break; + case NativeMethods.WM_KILLFOCUS: + _lastImmOpenStatus = NativeMethods.ImmGetOpenStatus(DefaultImc); + break; + case NativeMethods.WM_IME_NOTIFY: + IMENotify(wParam.ToInt32()); + if (!InputMethod.ShowOSImeWindow) + return true; + break; + case NativeMethods.WM_IME_STARTCOMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_STARTCOMPOSITION"); + IMEStartComposion(lParam.ToInt32()); + // Force to not show composition window, `lParam64 &= ~ISC_SHOWUICOMPOSITIONWINDOW` don't work sometime. + return true; + case NativeMethods.WM_IME_COMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_COMPOSITION"); + IMEComposition(lParam.ToInt32()); + break; + case NativeMethods.WM_IME_ENDCOMPOSITION: + //Debug.WriteLine("NativeMethods.WM_IME_ENDCOMPOSITION"); + IMEEndComposition(lParam.ToInt32()); + if (!InputMethod.ShowOSImeWindow) + return true; + break; + } + + return false; + } + + private void IMENotify(int WParam) + { + switch (WParam) + { + case NativeMethods.IMN_OPENCANDIDATE: + case NativeMethods.IMN_CHANGECANDIDATE: + IMEChangeCandidate(); + break; + case NativeMethods.IMN_CLOSECANDIDATE: + InputMethod.ClearCandidates(); + break; + default: + break; + } + } + + private void IMEChangeCandidate() + { + if (TextServicesLoader.ServicesInstalled) // TSF is enabled + { + if (!TextStore.Current.SupportUIElement) // But active IME not support UIElement + UpdateCandidates(); // We have to fetch candidate list here. + + return; + } + + // Normal candidate list fetch in IMM32 + UpdateCandidates(); + // Send event on candidate updates + InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value); + + if (InputMethod.CandidateList != null) + ArrayPool.Shared.Return(InputMethod.CandidateList); + } + + private unsafe void UpdateCandidates() + { + uint length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, IntPtr.Zero, 0); + if (length > 0) + { + IntPtr pointer = Marshal.AllocHGlobal((int)length); + length = NativeMethods.ImmGetCandidateList(DefaultImc, 0, pointer, length); + NativeMethods.CANDIDATELIST* cList = (NativeMethods.CANDIDATELIST*)pointer; + + var selection = (int)cList->dwSelection; + var pageStart = (int)cList->dwPageStart; + var pageSize = (int)cList->dwPageSize; + + selection -= pageStart; + + IMEString[] candidates = ArrayPool.Shared.Rent(pageSize); + + int i, j; + for (i = pageStart, j = 0; i < cList->dwCount && j < pageSize; i++, j++) + { + int sOffset = Marshal.ReadInt32(pointer, 24 + 4 * i); + candidates[j] = new IMEString(pointer + sOffset); + } + + //Debug.WriteLine("IMM========IMM"); + //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, candidates:", pageStart, pageSize, selection); + //for (int k = 0; k < candidates.Length; k++) + // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : ""); + //Debug.WriteLine("IMM++++++++IMM"); + + InputMethod.CandidatePageStart = pageStart; + InputMethod.CandidatePageSize = pageSize; + InputMethod.CandidateSelection = selection; + InputMethod.CandidateList = candidates; + + Marshal.FreeHGlobal(pointer); + } + } + + private void ClearComposition() + { + _compositionStringHandler.Clear(); + } + + private void IMEStartComposion(int lParam) + { + InputMethod.OnTextCompositionStarted(this); + ClearComposition(); + } + + private void IMEComposition(int lParam) + { + if (_compositionStringHandler.Update(lParam)) + { + _compositionCursorHandler.Update(); + + InputMethod.OnTextComposition(this, new IMEString(_compositionStringHandler.Values, _compositionStringHandler.Count), _compositionCursorHandler.Value); + } + } + + private void IMEEndComposition(int lParam) + { + InputMethod.ClearCandidates(); + ClearComposition(); + + InputMethod.OnTextCompositionEnded(this); + } + } +} diff --git a/Libraries/ImeSharp/ImmCompositionResultHandler.cs b/Libraries/ImeSharp/ImmCompositionResultHandler.cs new file mode 100644 index 000000000..69d3d651b --- /dev/null +++ b/Libraries/ImeSharp/ImmCompositionResultHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; +using ImeSharp.Native; + +namespace ImeSharp +{ + internal abstract class ImmCompositionResultHandler + { + protected IntPtr _imeContext; + + public int Flag { get; private set; } + + internal ImmCompositionResultHandler(IntPtr imeContext, int flag) + { + this.Flag = flag; + _imeContext = imeContext; + } + + internal virtual void Update() { } + + internal bool Update(int lParam) + { + if ((lParam & Flag) == Flag) + { + Update(); + return true; + } + return false; + } + } + + internal class ImmCompositionStringHandler : ImmCompositionResultHandler + { + internal const int BufferSize = 1024; + private byte[] _byteBuffer = new byte[BufferSize]; + private int _byteCount; + + private char[] _charBuffer = new char[BufferSize / 2]; + private int _charCount; + + public char[] Values { get { return _charBuffer; } } + public int Count { get { return _charCount; } } + + public char this[int index] + { + get + { + if (index >= _charCount || index < 0) + throw new ArgumentOutOfRangeException("index"); + + return _charBuffer[index]; + } + } + + internal ImmCompositionStringHandler(IntPtr imeContext, int flag) : base(imeContext, flag) + { + } + + public override string ToString() + { + if (_charCount <= 0) + return string.Empty; + + return new string(_charBuffer, 0, _charCount); + } + + internal void Clear() + { + Array.Clear(_byteBuffer, 0, _byteCount); + _byteCount = 0; + + Array.Clear(_charBuffer, 0, _charCount); + _charCount = 0; + } + + internal override void Update() + { + _byteCount = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0); + IntPtr pointer = Marshal.AllocHGlobal(_byteCount); + + try + { + Array.Clear(_byteBuffer, 0, _byteCount); + + if (_byteCount > 0) + { + NativeMethods.ImmGetCompositionString(_imeContext, Flag, pointer, _byteCount); + + Marshal.Copy(pointer, _byteBuffer, 0, _byteCount); + + Array.Clear(_charBuffer, 0, _charCount); + _charCount = Encoding.Unicode.GetChars(_byteBuffer, 0, _byteCount, _charBuffer, 0); + } + } + finally + { + Marshal.FreeHGlobal(pointer); + } + } + } + + internal class ImmCompositionIntHandler : ImmCompositionResultHandler + { + public int Value { get; private set; } + + internal ImmCompositionIntHandler(IntPtr imeContext, int flag) : base(imeContext, flag) { } + + public override string ToString() + { + return Value.ToString(); + } + + internal override void Update() + { + Value = NativeMethods.ImmGetCompositionString(_imeContext, Flag, IntPtr.Zero, 0); + } + } +} diff --git a/Libraries/ImeSharp/InputMethod.cs b/Libraries/ImeSharp/InputMethod.cs new file mode 100644 index 000000000..fa178cbd7 --- /dev/null +++ b/Libraries/ImeSharp/InputMethod.cs @@ -0,0 +1,246 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using ImeSharp.Native; + +namespace ImeSharp +{ + public static class InputMethod + { + private static IntPtr _windowHandle; + public static IntPtr WindowHandle { get { return _windowHandle; } } + + private static IntPtr _prevWndProc; + private static NativeMethods.WndProcDelegate _wndProcDelegate; + + private static TextServicesContext _textServicesContext; + internal static TextServicesContext TextServicesContext + { + get { return _textServicesContext; } + set { _textServicesContext = value; } + } + + private static TextStore _defaultTextStore; + internal static TextStore DefaultTextStore + { + get { return _defaultTextStore; } + set { _defaultTextStore = value; } + } + + private static Imm32Manager _defaultImm32Manager; + internal static Imm32Manager DefaultImm32Manager + { + get { return _defaultImm32Manager; } + set { _defaultImm32Manager = value; } + } + + private static bool _enabled; + public static bool Enabled + { + get { return _enabled; } + set + { + if (_enabled == value) return; + + _enabled = value; + + EnableOrDisableInputMethod(_enabled); + } + } + + internal static TsfSharp.Rect TextInputRect; + + /// + /// Set the position of the candidate window rendered by the OS. + /// Let the OS render the candidate window by set param "showOSImeWindow" to true on . + /// + public static void SetTextInputRect(int x, int y, int width, int height) + { + if (!_showOSImeWindow) return; + + TextInputRect.Left = x; + TextInputRect.Top = y; + TextInputRect.Right = x + width; + TextInputRect.Bottom = y + height; + + if (Imm32Manager.ImmEnabled) + Imm32Manager.Current.SetCandidateWindow(TextInputRect); + } + + private static bool _showOSImeWindow = false; + + /// + /// Return if let OS render IME Candidate window or not. + /// + public static bool ShowOSImeWindow { get { return _showOSImeWindow; } } + + internal static int CandidatePageStart; + internal static int CandidatePageSize; + internal static int CandidateSelection; + internal static IMEString[] CandidateList; + + internal static void ClearCandidates() + { + CandidateList = null; + CandidatePageStart = 0; + CandidatePageSize = 0; + CandidateSelection = 0; + } + + public static event EventHandler TextComposition; + public static event EventHandler TextInput; + public static event EventHandler CommitTextComposition; + + public static TextInputCallback TextInputCallback { get; set; } + public static TextCompositionCallback TextCompositionCallback { get; set; } + public static CommitTextCompositionCallback CommitTextCompositionCallback { get; set; } + + /// + /// Initialize InputMethod with a Window Handle. + /// Let the OS render the candidate window by set to true. + /// + public static void Initialize(IntPtr windowHandle, bool showOSImeWindow = true) + { + if (_windowHandle != IntPtr.Zero) + throw new InvalidOperationException("InputMethod can only be initialized once!"); + + _windowHandle = windowHandle; + _showOSImeWindow = showOSImeWindow; + + _wndProcDelegate = new NativeMethods.WndProcDelegate(WndProc); + _prevWndProc = (IntPtr)NativeMethods.SetWindowLongPtr(_windowHandle, NativeMethods.GWL_WNDPROC, + Marshal.GetFunctionPointerForDelegate(_wndProcDelegate)); + } + + internal static void OnTextInput(object sender, char character) + { + if (TextInput != null) + TextInput.Invoke(sender, new IMETextInputEventArgs(character)); + + if (TextInputCallback != null) + TextInputCallback(character); + } + + // Some Chinese IME only send composition start event but no composition update event. + // We need this to ensure candidate window position can be set in time. + internal static void OnTextCompositionStarted(object sender) + { + if (TextComposition != null) + TextComposition.Invoke(sender, new IMETextCompositionEventArgs(IMEString.Empty, 0)); + + if (TextCompositionCallback != null) + TextCompositionCallback(IMEString.Empty, 0, null, 0, 0, 0); + } + + // On text composition update. + internal static void OnTextComposition(object sender, IMEString compositionText, int cursorPos) + { + if (compositionText.Count == 0) // Crash guard + cursorPos = 0; + + if (cursorPos > compositionText.Count) // Another crash guard + cursorPos = compositionText.Count; + + if (TextComposition != null) + { + TextComposition.Invoke(sender, + new IMETextCompositionEventArgs(compositionText, cursorPos, CandidateList, CandidatePageStart, CandidatePageSize, CandidateSelection)); + } + + if (TextCompositionCallback != null) + TextCompositionCallback(compositionText, cursorPos, CandidateList, CandidatePageStart, CandidatePageSize, CandidateSelection); + } + + internal static void OnTextCompositionResult(object sender, string compositionResult) + { + if (CommitTextComposition != null) + CommitTextComposition.Invoke(sender, compositionResult); + + if (CommitTextCompositionCallback != null) + CommitTextCompositionCallback(compositionResult); + } + + internal static void OnTextCompositionEnded(object sender) + { + if (TextComposition != null) + TextComposition.Invoke(sender, new IMETextCompositionEventArgs(IMEString.Empty, 0)); + + if (TextCompositionCallback != null) + TextCompositionCallback(IMEString.Empty, 0, null, 0, 0, 0); + } + + private static void EnableOrDisableInputMethod(bool bEnabled) + { + // InputMethod enable/disabled status was changed on the current focus Element. + if (TextServicesLoader.ServicesInstalled) + { + if (bEnabled) + TextServicesContext.Current.SetFocusOnDefaultTextStore(); + else + TextServicesContext.Current.SetFocusOnEmptyDim(); + } + + // Under IMM32 enabled system, we associate default hIMC or null hIMC. + if (Imm32Manager.ImmEnabled) + { + if (bEnabled) + Imm32Manager.Current.Enable(); + else + Imm32Manager.Current.Disable(); + } + } + + private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (Imm32Manager.ImmEnabled) + { + if (Imm32Manager.Current.ProcessMessage(hWnd, msg, ref wParam, ref lParam)) + return IntPtr.Zero; + } + + switch (msg) + { + case NativeMethods.WM_DESTROY: + TextServicesContext.Current.Uninitialize(true); + break; + case NativeMethods.WM_CHAR: + { + if (InputMethod.Enabled) + InputMethod.OnTextInput(null, (char)wParam.ToInt32()); + + break; + } + } + + return NativeMethods.CallWindowProc(_prevWndProc, hWnd, msg, wParam, lParam); + } + + /// + /// Custom windows message pumping to fix frame stuck issue. + /// Normally, you need call this method in handler. + /// + public static void PumpMessage() + { + if (!Enabled) return; + if (!TextServicesLoader.ServicesInstalled) return; + + bool result; + var msg = new NativeMethods.NativeMessage(); + + do + { + result = NativeMethods.PeekMessage(out msg, _windowHandle, 0, 0, NativeMethods.PM_REMOVE); + + if (result) + { + NativeMethods.TranslateMessage(ref msg); + NativeMethods.DispatchMessage(ref msg); + } + } while (result); + + NativeMethods.PostMessage(_windowHandle, NativeMethods.WM_NULL, IntPtr.Zero, IntPtr.Zero); + } + } +} diff --git a/Libraries/ImeSharp/Native/NativeMethods.cs b/Libraries/ImeSharp/Native/NativeMethods.cs new file mode 100644 index 000000000..1e3d6f901 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeMethods.cs @@ -0,0 +1,149 @@ +using System; +using System.Text; +using System.Runtime.InteropServices; + +namespace ImeSharp.Native +{ + public partial class NativeMethods + { + #region Constants + + public const int S_OK = 0x00000000; + public const int S_FALSE = 0x00000001; + public const int E_FAIL = unchecked((int)0x80004005); + public const int E_INVALIDARG = unchecked((int)0x80070057); + public const int E_NOTIMPL = unchecked((int)0x80004001); + + public const int WM_KEYFIRST = 0x0100; + public const int WM_KEYDOWN = 0x0100; + public const int WM_KEYUP = 0x0101; + public const int WM_CHAR = 0x0102; + public const int WM_DEADCHAR = 0x0103; + public const int WM_SYSKEYDOWN = 0x0104; + public const int WM_SYSKEYUP = 0x0105; + public const int WM_SYSCHAR = 0x0106; + public const int WM_SYSDEADCHAR = 0x0107; + public const int WM_UNICHAR = 0x0109; + public const int WM_KEYLAST = 0x0109; + public const int UNICODE_NOCHAR = 0xFFFF; + + public const int WM_NOTIFY = 0x004E; + public const int WM_INPUTLANGCHANGEREQUEST = 0x0050; + public const int WM_INPUTLANGCHANGE = 0x0051; + public const int WM_TCARD = 0x0052; + public const int WM_HELP = 0x0053; + public const int WM_USERCHANGED = 0x0054; + public const int WM_NOTIFYFORMAT = 0x0055; + + public const int GWL_WNDPROC = -4; + + public const int WM_ACTIVATE = 0x0006; + // WM_ACTIVATE state values + public const int WA_INACTIVE = 0; + public const int WA_ACTIVE = 1; + public const int WA_CLICKACTIVE = 2; + + public const int WM_SETFOCUS = 0x0007; + public const int WM_KILLFOCUS = 0x0008; + + public const int WM_DESTROY = 0x0002; + public const int WM_NULL = 0x0000; + public const int WM_QUIT = 0x0012; + + public const int CLSCTX_INPROC_SERVER = 0x1; + + public const int PM_NOREMOVE = 0x0000; + public const int PM_REMOVE = 0x0001; + public const int PM_NOYIELD = 0x0002; + + #endregion Constants + + #region Structs + + [StructLayout(LayoutKind.Sequential)] + public struct NativeMessage + { + public IntPtr handle; + public uint msg; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public int ptX; + public int ptY; + } + + #endregion + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(SM nIndex); + + // We have this wrapper because casting IntPtr to int may + // generate OverflowException when one of high 32 bits is set. + public static int IntPtrToInt32(IntPtr intPtr) + { + return unchecked((int)intPtr.ToInt64()); + } + + [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] + public static extern IntPtr GetKeyboardLayout(int dwLayout); + + public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + // This static method is required because legacy OSes do not support + public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 8) + return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + else + return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32())); + } + + [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Unicode)] + private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Unicode)] + private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hwnd, out TsfSharp.Rect lpRect); + + [DllImport("user32", ExactSpelling = true, SetLastError = true)] + public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref TsfSharp.Rect rect, [MarshalAs(UnmanagedType.U4)] int cPoints); + + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, StringBuilder lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, ref IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + public static extern bool TranslateMessage(ref NativeMessage lpMsg); + + [DllImport("user32.dll")] + public static extern IntPtr DispatchMessage(ref NativeMessage lpmsg); + + [DllImport("User32.dll", CharSet = CharSet.Unicode)] + public static extern bool PeekMessage(out NativeMessage msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags); + + + [DllImport("ole32.dll", ExactSpelling = true, EntryPoint = "CoCreateInstance", PreserveSig = true)] + public static extern int CoCreateInstance([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pUnkOuter, int dwClsContext, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); + + } +} diff --git a/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs b/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs new file mode 100644 index 000000000..701d6aff2 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeMethodsIMM32.cs @@ -0,0 +1,156 @@ +using System; +using System.Runtime.InteropServices; + +namespace ImeSharp.Native +{ + public partial class NativeMethods + { + #region Constants + + public const int WM_IME_SETCONTEXT = 0x0281; + public const int WM_IME_NOTIFY = 0x0282; + public const int WM_IME_CONTROL = 0x0283; + public const int WM_IME_COMPOSITIONFULL = 0x0284; + public const int WM_IME_SELECT = 0x0285; + public const int WM_IME_CHAR = 0x0286; + public const int WM_IME_REQUEST = 0x0288; + public const int WM_IME_KEYDOWN = 0x0290; + public const int WM_IME_KEYUP = 0x0291; + public const int WM_IME_STARTCOMPOSITION = 0x010D; + public const int WM_IME_ENDCOMPOSITION = 0x010E; + public const int WM_IME_COMPOSITION = 0x010F; + public const int WM_IME_KEYLAST = 0x010F; + + // wParam of report message WM_IME_NOTIFY + public const int IMN_CLOSESTATUSWINDOW = 0x0001; + public const int IMN_OPENSTATUSWINDOW = 0x0002; + public const int IMN_CHANGECANDIDATE = 0x0003; + public const int IMN_CLOSECANDIDATE = 0x0004; + public const int IMN_OPENCANDIDATE = 0x0005; + public const int IMN_SETCONVERSIONMODE = 0x0006; + public const int IMN_SETSENTENCEMODE = 0x0007; + public const int IMN_SETOPENSTATUS = 0x0008; + public const int IMN_SETCANDIDATEPOS = 0x0009; + public const int IMN_SETCOMPOSITIONFONT = 0x000A; + public const int IMN_SETCOMPOSITIONWINDOW = 0x000B; + public const int IMN_SETSTATUSWINDOWPOS = 0x000C; + public const int IMN_GUIDELINE = 0x000D; + public const int IMN_PRIVATE = 0x000E; + + // wParam of report message WM_IME_REQUEST + public const int IMR_COMPOSITIONWINDOW = 0x0001; + public const int IMR_CANDIDATEWINDOW = 0x0002; + public const int IMR_COMPOSITIONFONT = 0x0003; + public const int IMR_RECONVERTSTRING = 0x0004; + public const int IMR_CONFIRMRECONVERTSTRING = 0x0005; + public const int IMR_QUERYCHARPOSITION = 0x0006; + public const int IMR_DOCUMENTFEED = 0x0007; + + // parameter of ImmGetCompositionString + public const int GCS_COMPREADSTR = 0x0001; + public const int GCS_COMPREADATTR = 0x0002; + public const int GCS_COMPREADCLAUSE = 0x0004; + public const int GCS_COMPSTR = 0x0008; + public const int GCS_COMPATTR = 0x0010; + public const int GCS_COMPCLAUSE = 0x0020; + public const int GCS_CURSORPOS = 0x0080; + public const int GCS_DELTASTART = 0x0100; + public const int GCS_RESULTREADSTR = 0x0200; + public const int GCS_RESULTREADCLAUSE = 0x0400; + public const int GCS_RESULTSTR = 0x0800; + public const int GCS_RESULTCLAUSE = 0x1000; + + public const int GCS_COMP = (GCS_COMPSTR | GCS_COMPATTR | GCS_COMPCLAUSE); + public const int GCS_COMPREAD = (GCS_COMPREADSTR | GCS_COMPREADATTR | GCS_COMPREADCLAUSE); + public const int GCS_RESULT = (GCS_RESULTSTR | GCS_RESULTCLAUSE); + public const int GCS_RESULTREAD = (GCS_RESULTREADSTR | GCS_RESULTREADCLAUSE); + + public const int CFS_CANDIDATEPOS = 0x0040; + public const int CFS_POINT = 0x0002; + public const int CFS_EXCLUDE = 0x0080; + + // lParam for WM_IME_SETCONTEXT + public const long ISC_SHOWUICANDIDATEWINDOW = 0x00000001; + public const long ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000; + public const long ISC_SHOWUIGUIDELINE = 0x40000000; + public const long ISC_SHOWUIALLCANDIDATEWINDOW = 0x0000000F; + public const long ISC_SHOWUIALL = 0xC000000F; + + #endregion Constants + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct CANDIDATELIST + { + public uint dwSize; + public uint dwStyle; + public uint dwCount; + public uint dwSelection; + public uint dwPageStart; + public uint dwPageSize; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.U4)] + public fixed uint dwOffset[1]; + } + + // CANDIDATEFORM structures + [StructLayout(LayoutKind.Sequential)] + public struct CANDIDATEFORM + { + public int dwIndex; + public int dwStyle; + public TsfSharp.Point ptCurrentPos; + public TsfSharp.Rect rcArea; + } + + // COMPOSITIONFORM structures + [StructLayout(LayoutKind.Sequential)] + public struct COMPOSITIONFORM + { + public int dwStyle; + public TsfSharp.Point ptCurrentPos; + public TsfSharp.Rect rcArea; + } + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmCreateContext(); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern bool ImmDestroyContext(IntPtr hIMC); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); + + [DllImport("imm32.dll", CharSet = CharSet.Unicode)] + public static extern uint ImmGetCandidateList(IntPtr hIMC, uint deIndex, IntPtr candidateList, uint dwBufLen); + + [DllImport("imm32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int ImmGetCompositionString(IntPtr hIMC, int CompositionStringFlag, IntPtr buffer, int bufferLength); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern IntPtr ImmGetContext(IntPtr hWnd); + + [DllImport("Imm32.dll", SetLastError = true)] + public static extern bool ImmGetOpenStatus(IntPtr hIMC); + + [DllImport("Imm32.dll", SetLastError = true)] + public static extern bool ImmSetOpenStatus(IntPtr hIMC, bool open); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern bool ImmSetCandidateWindow(IntPtr hIMC, ref CANDIDATEFORM candidateForm); + + [DllImport("imm32.dll", SetLastError = true)] + public static extern int ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM compForm); + + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyCaret(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetCaretPos(int x, int y); + } +} diff --git a/Libraries/ImeSharp/Native/NativeValues.cs b/Libraries/ImeSharp/Native/NativeValues.cs new file mode 100644 index 000000000..1c0dd9a36 --- /dev/null +++ b/Libraries/ImeSharp/Native/NativeValues.cs @@ -0,0 +1,95 @@ +using System; + +namespace ImeSharp.Native +{ + /// + /// SystemMetrics. SM_* + /// + public enum SM + { + CXSCREEN = 0, + CYSCREEN = 1, + CXVSCROLL = 2, + CYHSCROLL = 3, + CYCAPTION = 4, + CXBORDER = 5, + CYBORDER = 6, + CXFIXEDFRAME = 7, + CYFIXEDFRAME = 8, + CYVTHUMB = 9, + CXHTHUMB = 10, + CXICON = 11, + CYICON = 12, + CXCURSOR = 13, + CYCURSOR = 14, + CYMENU = 15, + CXFULLSCREEN = 16, + CYFULLSCREEN = 17, + CYKANJIWINDOW = 18, + MOUSEPRESENT = 19, + CYVSCROLL = 20, + CXHSCROLL = 21, + DEBUG = 22, + SWAPBUTTON = 23, + CXMIN = 28, + CYMIN = 29, + CXSIZE = 30, + CYSIZE = 31, + CXFRAME = 32, + CXSIZEFRAME = CXFRAME, + CYFRAME = 33, + CYSIZEFRAME = CYFRAME, + CXMINTRACK = 34, + CYMINTRACK = 35, + CXDOUBLECLK = 36, + CYDOUBLECLK = 37, + CXICONSPACING = 38, + CYICONSPACING = 39, + MENUDROPALIGNMENT = 40, + PENWINDOWS = 41, + DBCSENABLED = 42, + CMOUSEBUTTONS = 43, + SECURE = 44, + CXEDGE = 45, + CYEDGE = 46, + CXMINSPACING = 47, + CYMINSPACING = 48, + CXSMICON = 49, + CYSMICON = 50, + CYSMCAPTION = 51, + CXSMSIZE = 52, + CYSMSIZE = 53, + CXMENUSIZE = 54, + CYMENUSIZE = 55, + ARRANGE = 56, + CXMINIMIZED = 57, + CYMINIMIZED = 58, + CXMAXTRACK = 59, + CYMAXTRACK = 60, + CXMAXIMIZED = 61, + CYMAXIMIZED = 62, + NETWORK = 63, + CLEANBOOT = 67, + CXDRAG = 68, + CYDRAG = 69, + SHOWSOUNDS = 70, + CXMENUCHECK = 71, + CYMENUCHECK = 72, + SLOWMACHINE = 73, + MIDEASTENABLED = 74, + MOUSEWHEELPRESENT = 75, + XVIRTUALSCREEN = 76, + YVIRTUALSCREEN = 77, + CXVIRTUALSCREEN = 78, + CYVIRTUALSCREEN = 79, + CMONITORS = 80, + SAMEDISPLAYFORMAT = 81, + IMMENABLED = 82, + CXFOCUSBORDER = 83, + CYFOCUSBORDER = 84, + TABLETPC = 86, + MEDIACENTER = 87, + REMOTESESSION = 0x1000, + REMOTECONTROL = 0x2001, + } +} diff --git a/Libraries/ImeSharp/SafeSystemMetrics.cs b/Libraries/ImeSharp/SafeSystemMetrics.cs new file mode 100644 index 000000000..a2e76f5b2 --- /dev/null +++ b/Libraries/ImeSharp/SafeSystemMetrics.cs @@ -0,0 +1,58 @@ +using System; +using ImeSharp.Native; + +namespace ImeSharp +{ + /// + /// Contains properties that are queries into the system's various settings. + /// + internal sealed class SafeSystemMetrics + { + + private SafeSystemMetrics() + { + } + + /// + /// Maps to SM_CXDOUBLECLK + /// + public static int DoubleClickDeltaX + { + get { return NativeMethods.GetSystemMetrics(SM.CXDOUBLECLK); } + } + + /// + /// Maps to SM_CYDOUBLECLK + /// + public static int DoubleClickDeltaY + { + get { return NativeMethods.GetSystemMetrics(SM.CYDOUBLECLK); } + } + + + /// + /// Maps to SM_CXDRAG + /// + public static int DragDeltaX + { + get { return NativeMethods.GetSystemMetrics(SM.CXDRAG); } + } + + /// + /// Maps to SM_CYDRAG + /// + public static int DragDeltaY + { + get { return NativeMethods.GetSystemMetrics(SM.CYDRAG); } + } + + /// + /// Is an IMM enabled ? Maps to SM_IMMENABLED + /// + public static bool IsImmEnabled + { + get { return (NativeMethods.GetSystemMetrics(SM.IMMENABLED) != 0); } + } + + } +} diff --git a/Libraries/ImeSharp/TextInputCallbacks.cs b/Libraries/ImeSharp/TextInputCallbacks.cs new file mode 100644 index 000000000..c37837927 --- /dev/null +++ b/Libraries/ImeSharp/TextInputCallbacks.cs @@ -0,0 +1,6 @@ +namespace ImeSharp +{ + public delegate void TextInputCallback(char character); + public delegate void TextCompositionCallback(IMEString compositionText, int cursorPosition, IMEString[] candidateList, int candidatePageStart, int candidatePageSize, int candidateSelection); + public delegate void CommitTextCompositionCallback(string text); +} diff --git a/Libraries/ImeSharp/TextServicesContext.cs b/Libraries/ImeSharp/TextServicesContext.cs new file mode 100644 index 000000000..6ce846528 --- /dev/null +++ b/Libraries/ImeSharp/TextServicesContext.cs @@ -0,0 +1,374 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Diagnostics; +using ImeSharp.Native; +using TsfSharp; + +namespace ImeSharp +{ + //------------------------------------------------------ + // + // TextServicesContext class + // + //------------------------------------------------------ + + /// + /// This class manages the ITfThreadMgr, EmptyDim and the reference to + /// the default TextStore. + /// + /// + /// + internal class TextServicesContext + { + public const int TF_POPF_ALL = 0x0001; + public const int TF_INVALID_COOKIE = -1; + public static readonly Guid IID_ITfUIElementSink = new Guid(0xea1ea136, 0x19df, 0x11d7, 0xa6, 0xd2, 0x00, 0x06, 0x5b, 0x84, 0x43, 0x5c); + public static readonly Guid IID_ITfTextEditSink = new Guid(0x8127d409, 0xccd3, 0x4683, 0x96, 0x7a, 0xb4, 0x3d, 0x5b, 0x48, 0x2b, 0xf7); + + + public static TextServicesContext Current + { + get + { + if (InputMethod.TextServicesContext == null) + InputMethod.TextServicesContext = new TextServicesContext(); + + return InputMethod.TextServicesContext; + } + } + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + /// + /// Instantiates a TextServicesContext. + /// + private TextServicesContext() + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!"); + } + + #endregion Constructors + + //------------------------------------------------------ + // + // public Methods + // + //------------------------------------------------------ + + #region public Methods + + /// + /// Releases all unmanaged resources allocated by the + /// TextServicesContext. + /// + /// + /// if appDomainShutdown == false, this method must be called on the + /// Dispatcher thread. Otherwise, the caller is an AppDomain.Shutdown + /// listener, and is calling from a worker thread. + /// + public void Uninitialize(bool appDomainShutdown) + { + // Unregister DefaultTextStore. + if (_defaultTextStore != null) + { + UnadviseSinks(); + if (_defaultTextStore.DocumentManager != null) + { + _defaultTextStore.DocumentManager.Pop(TF_POPF_ALL); + _defaultTextStore.DocumentManager.Dispose(); + _defaultTextStore.DocumentManager = null; + } + + _defaultTextStore = null; + } + + // Free up any remaining textstores. + if (_istimactivated == true) + { + // Shut down the thread manager when the last TextStore goes away. + // On XP, if we're called on a worker thread (during AppDomain shutdown) + // we can't call call any methods on _threadManager. The problem is + // that there's no proxy registered for ITfThreadMgr on OS versions + // previous to Vista. Not calling Deactivate will leak the IMEs, but + // in practice (1) they're singletons, so it's not unbounded; and (2) + // most applications will share the thread with other AppDomains that + // have a UI, in which case the IME won't be released until the process + // shuts down in any case. In theory we could also work around this + // problem by creating our own XP proxy/stub implementation, which would + // be added to WPF setup.... + if (!appDomainShutdown || System.Environment.OSVersion.Version.Major >= 6) + { + _threadManager.Deactivate(); + } + _istimactivated = false; + } + + // Release the empty dim. + if (_dimEmpty != null) + { + if (_dimEmpty != null) + { + _dimEmpty.Dispose(); + } + _dimEmpty = null; + } + + // Release the ThreadManager. + // We don't do this in UnregisterTextStore because someone may have + // called get_ThreadManager after the last TextStore was unregistered. + if (_threadManager != null) + { + if (_threadManager != null) + { + _threadManager.Dispose(); + } + _threadManager = null; + } + } + + // Called by framework's TextStore class. This method registers a + // document with TSF. The TextServicesContext must maintain this list + // to ensure all native resources are released after gc or uninitialization. + public void RegisterTextStore(TextStore defaultTextStore) + { + _defaultTextStore = defaultTextStore; + + ITfThreadMgrEx threadManager = ThreadManager; + + if (threadManager != null) + { + ITfDocumentMgr doc; + int editCookie = TF_INVALID_COOKIE; + + // Activate TSF on this thread if this is the first TextStore. + if (_istimactivated == false) + { + //temp variable created to retrieve the value + // which is then stored in the critical data. + if (InputMethod.ShowOSImeWindow) + _clientId = threadManager.Activate(); + else + _clientId = threadManager.ActivateEx(TfTmaeFlags.Uielementenabledonly); + + _istimactivated = true; + } + + // Create a TSF document. + doc = threadManager.CreateDocumentMgr(); + _defaultTextStore.DocumentManager = doc; + + doc.CreateContext(_clientId, 0 /* flags */, _defaultTextStore, out _editContext, out editCookie); + _defaultTextStore.EditCookie = editCookie; + _contextOwnerServices = _editContext.QueryInterface(); + + doc.Push(_editContext); + + AdviseSinks(); + } + } + + + public void SetFocusOnDefaultTextStore() + { + SetFocusOnDim(TextStore.Current.DocumentManager); + } + + public void SetFocusOnEmptyDim() + { + SetFocusOnDim(EmptyDocumentManager); + } + + + #endregion public Methods + + //------------------------------------------------------ + // + // public Properties + // + //------------------------------------------------------ + + /// + /// The default ITfThreadMgrEx object. + /// + public ITfThreadMgrEx ThreadManager + { + // The ITfThreadMgr for this thread. + get + { + if (_threadManager == null) + { + ITfThreadMgr threadMgr = null; + try + { + // This might fail in CoreRT + threadMgr = Tsf.GetThreadMgr(); + } + catch (SharpGen.Runtime.SharpGenException) + { + threadMgr = null; + } + + // Dispose previous ITfThreadMgr in case something weird happens + if (threadMgr != null) + { + if (threadMgr.IsThreadFocus) + threadMgr.Deactivate(); + threadMgr.Dispose(); + } + + _threadManager = TextServicesLoader.Load(); + + _uiElementMgr = _threadManager.QueryInterface(); + } + + return _threadManager; + } + } + + /// + /// Return the created ITfContext object. + /// + public ITfContext EditContext + { + get { return _editContext; } + } + + /// + /// Return the created ITfUIElementMgr object. + /// + public ITfUIElementMgr UIElementMgr + { + get { return _uiElementMgr; } + } + + /// + /// Return the created ITfContextOwnerServices object. + /// + public ITfContextOwnerServices ContextOwnerServices + { + get { return _contextOwnerServices; } + } + + //------------------------------------------------------ + // + // public Events + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + private void SetFocusOnDim(ITfDocumentMgr dim) + { + ITfThreadMgrEx threadmgr = ThreadManager; + + if (threadmgr != null) + { + ITfDocumentMgr prevDocMgr = threadmgr.AssociateFocus(InputMethod.WindowHandle, dim); + } + } + + private void AdviseSinks() + { + var source = _uiElementMgr.QueryInterface(); + var guid = IID_ITfUIElementSink; + int sinkCookie = source.AdviseSink(guid, _defaultTextStore); + _defaultTextStore.UIElementSinkCookie = sinkCookie; + source.Dispose(); + + source = _editContext.QueryInterface(); + guid = IID_ITfTextEditSink; + sinkCookie = source.AdviseSink(guid, _defaultTextStore); + _defaultTextStore.TextEditSinkCookie = sinkCookie; + source.Dispose(); + } + + private void UnadviseSinks() + { + var source = _uiElementMgr.QueryInterface(); + + if (_defaultTextStore.UIElementSinkCookie != TF_INVALID_COOKIE) + { + source.UnadviseSink(_defaultTextStore.UIElementSinkCookie); + _defaultTextStore.UIElementSinkCookie = TF_INVALID_COOKIE; + } + source.Dispose(); + + source = _editContext.QueryInterface(); + if (_defaultTextStore.TextEditSinkCookie != TF_INVALID_COOKIE) + { + source.UnadviseSink(_defaultTextStore.TextEditSinkCookie); + _defaultTextStore.TextEditSinkCookie = TF_INVALID_COOKIE; + } + source.Dispose(); + } + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + // Create an empty dim on demand. + private ITfDocumentMgr EmptyDocumentManager + { + get + { + if (_dimEmpty == null) + { + ITfThreadMgrEx threadManager = ThreadManager; + if (threadManager == null) + { + return null; + } + + ITfDocumentMgr dimEmptyTemp; + // Create a TSF document. + dimEmptyTemp = threadManager.CreateDocumentMgr(); + _dimEmpty = dimEmptyTemp; + } + return _dimEmpty; + } + } + + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + + private TextStore _defaultTextStore; + + private ITfContext _editContext; + private ITfUIElementMgr _uiElementMgr; + private ITfContextOwnerServices _contextOwnerServices; + + // This is true if thread manager is activated. + private bool _istimactivated; + + // The root TSF object, created on demand. + private ITfThreadMgrEx _threadManager; + + // TSF ClientId from Activate call. + private int _clientId; + + // The empty dim for this thread. Created on demand. + private ITfDocumentMgr _dimEmpty; + + #endregion Private Fields + } +} diff --git a/Libraries/ImeSharp/TextServicesLoader.cs b/Libraries/ImeSharp/TextServicesLoader.cs new file mode 100644 index 000000000..ff2037b57 --- /dev/null +++ b/Libraries/ImeSharp/TextServicesLoader.cs @@ -0,0 +1,338 @@ +using System; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Threading; +using Microsoft.Win32; +using ImeSharp.Native; +using TsfSharp; + +namespace ImeSharp +{ + // Creates ITfThreadMgr instances, the root object of the Text Services + // Framework. + internal class TextServicesLoader + { + public static readonly Guid CLSID_TF_ThreadMgr = new Guid("529a9e6b-6587-4f23-ab9e-9c7d683e3c50"); + public static readonly Guid IID_ITfThreadMgr = new Guid("aa80e801-2021-11d2-93e0-0060b067b86e"); + public static readonly Guid IID_ITfThreadMgrEx = new Guid("3e90ade3-7594-4cb0-bb58-69628f5f458c"); + public static readonly Guid IID_ITfThreadMgr2 = new Guid("0AB198EF-6477-4EE8-8812-6780EDB82D5E"); + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + // Private ctor to prevent anyone from instantiating this static class. + private TextServicesLoader() { } + + #endregion Constructors + + //------------------------------------------------------ + // + // public Properties + // + //------------------------------------------------------ + + #region public Properties + + /// + /// Loads an instance of the Text Services Framework. + /// + /// + /// May return null if no text services are available. + /// + public static ITfThreadMgrEx Load() + { + if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) + Debug.WriteLine("CRASH: ImeSharp won't work on MTA thread!!!"); + + if (ServicesInstalled) + { + // NB: a COMException here means something went wrong initialzing Cicero. + // Cicero will throw an exception if it doesn't think it should have been + // loaded (no TIPs to run), you can check that in msctf.dll's NoTipsInstalled + // which lives in nt\windows\advcore\ctf\lib\immxutil.cpp. If that's the + // problem, ServicesInstalled is out of sync with Cicero's thinking. + IntPtr ret; + var hr = NativeMethods.CoCreateInstance(CLSID_TF_ThreadMgr, + IntPtr.Zero, + NativeMethods.CLSCTX_INPROC_SERVER, + IID_ITfThreadMgrEx, out ret); + + if (hr == NativeMethods.S_OK) + return new ITfThreadMgrEx(ret); + } + + return null; + } + + /// + /// return true if current OS version is Windows 7 or below. + /// + public static bool IsWindows7OrBelow() + { + if (Environment.OSVersion.Version.Major <= 5) + return true; + + if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor <= 1) + return true; + + return false; + } + + /// + /// Informs the caller if text services are installed for the current user. + /// + /// + /// true if one or more text services are installed for the current user, otherwise false. + /// + /// + /// If this method returns false, TextServicesLoader.Load is guarenteed to return null. + /// Callers can use this information to avoid overhead that would otherwise be + /// required to support text services. + /// + public static bool ServicesInstalled + { + get + { + lock (s_servicesInstalledLock) + { + if (s_servicesInstalled == InstallState.Unknown) + { + s_servicesInstalled = TIPsWantToRun() ? InstallState.Installed : InstallState.NotInstalled; + } + } + + return (s_servicesInstalled == InstallState.Installed); + } + } + + #endregion public Properties + + //------------------------------------------------------ + // + // public Events + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + #region Private Methods + + // + // This method tries to stop Avalon from loading Cicero when there are no TIPs to run. + // The perf tradeoff is a typically small number of registry checks versus loading and + // initializing cicero. + // + // The Algorithm: + // + // Do a quick check vs. the global disable flag, return false if it is set. + // For each key under HKLM\SOFTWARE\Microsoft\CTF\TIP (a TIP or category clsid) + // If the the key has a LanguageProfile subkey (it's a TIP clsid) + // Iterate under the matching TIP entry in HKCU. + // For each key under the LanguageProfile (a particular LANGID) + // For each key under the LANGID (an assembly GUID) + // Try to read the Enable value. + // If the value is set non-zero, then stop all processing and return true. + // If the value is set zero, continue. + // If the value does not exist, continue (default is disabled). + // If any Enable values were found under HKCU for the TIP, then stop all processing and return false. + // Else, no Enable values have been found thus far and we keep going to investigate HKLM. + // Iterate under the TIP entry in HKLM. + // For each key under the LanguageProfile (a particular LANGID) + // For each key under the LANGID (an assembly GUID) + // Try to read the Enable value. + // If the value is set non-zero, then stop all processing and return true. + // If the value does not exist, then stop all processing and return true (default is enabled). + // If the value is set zero, continue. + // If we finish iterating all entries under HKLM without returning true, return false. + // + + private static bool TIPsWantToRun() + { + object obj; + RegistryKey key; + bool tipsWantToRun = false; + + key = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\CTF", false); + + // Is cicero disabled completely for the current user? + if (key != null) + { + obj = key.GetValue("Disable Thread Input Manager"); + + if (obj is int && (int)obj != 0) + return false; + } + + // Loop through all the TIP entries for machine and current user. + tipsWantToRun = IterateSubKeys(Registry.LocalMachine, "SOFTWARE\\Microsoft\\CTF\\TIP", new IterateHandler(SingleTIPWantsToRun), true) == EnableState.Enabled; + + return tipsWantToRun; + } + + // Returns EnableState.Enabled if one or more TIPs are installed and + // enabled for the current user. + private static EnableState SingleTIPWantsToRun(RegistryKey keyLocalMachine, string subKeyName, bool localMachine) + { + EnableState result; + + if (subKeyName.Length != CLSIDLength) + return EnableState.Disabled; + + // We want subkey\LanguageProfile key. + // Loop through all the langid entries for TIP. + + // First, check current user. + result = IterateSubKeys(Registry.CurrentUser, "SOFTWARE\\Microsoft\\CTF\\TIP\\" + subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), false); + + // Any explicit value short circuits the process. + // Otherwise check local machine. + if (result == EnableState.None || result == EnableState.Error) + { + result = IterateSubKeys(keyLocalMachine, subKeyName + "\\LanguageProfile", new IterateHandler(IsLangidEnabled), true); + + if (result == EnableState.None) + { + result = EnableState.Enabled; + } + } + + return result; + } + + // Returns EnableState.Enabled if the supplied subkey is a valid LANGID key with enabled + // cicero assembly. + private static EnableState IsLangidEnabled(RegistryKey key, string subKeyName, bool localMachine) + { + if (subKeyName.Length != LANGIDLength) + return EnableState.Error; + + // Loop through all the assembly entries for the langid + return IterateSubKeys(key, subKeyName, new IterateHandler(IsAssemblyEnabled), localMachine); + } + + // Returns EnableState.Enabled if the supplied assembly key is enabled. + private static EnableState IsAssemblyEnabled(RegistryKey key, string subKeyName, bool localMachine) + { + RegistryKey subKey; + object obj; + + if (subKeyName.Length != CLSIDLength) + return EnableState.Error; + + // Open the local machine assembly key. + subKey = key.OpenSubKey(subKeyName); + + if (subKey == null) + return EnableState.Error; + + // Try to read the "Enable" value. + obj = subKey.GetValue("Enable"); + + if (obj is int) + { + return ((int)obj == 0) ? EnableState.Disabled : EnableState.Enabled; + } + + return EnableState.None; + } + + // Calls the supplied delegate on each of the children of keyBase. + private static EnableState IterateSubKeys(RegistryKey keyBase, string subKey, IterateHandler handler, bool localMachine) + { + RegistryKey key; + string[] subKeyNames; + EnableState state; + + key = keyBase.OpenSubKey(subKey, false); + + if (key == null) + return EnableState.Error; + + subKeyNames = key.GetSubKeyNames(); + state = EnableState.Error; + + foreach (string name in subKeyNames) + { + switch (handler(key, name, localMachine)) + { + case EnableState.Error: + break; + case EnableState.None: + if (localMachine) // For lm, want to return here right away. + return EnableState.None; + + // For current user, remember that we found no Enable value. + if (state == EnableState.Error) + { + state = EnableState.None; + } + break; + case EnableState.Disabled: + state = EnableState.Disabled; + break; + case EnableState.Enabled: + return EnableState.Enabled; + } + } + + return state; + } + + #endregion Private Methods + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + + // String consts used to validate registry entires. + private const int CLSIDLength = 38; // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + private const int LANGIDLength = 10; // 0x12345678 + + // Status of a TIP assembly. + private enum EnableState + { + Error, // Invalid entry. + None, // No explicit Enable entry on the assembly. + Enabled, // Assembly is enabled. + Disabled // Assembly is disabled. + }; + + // Callback delegate for the IterateSubKeys method. + private delegate EnableState IterateHandler(RegistryKey key, string subKeyName, bool localMachine); + + // Install state. + private enum InstallState + { + Unknown, // Haven't checked to see if any TIPs are installed yet. + Installed, // Checked and installed. + NotInstalled // Checked and not installed. + } + + // Cached install state value. + // Writes are not thread safe, but we don't mind the neglible perf hit + // of potentially writing it twice. + private static InstallState s_servicesInstalled = InstallState.Unknown; + private static object s_servicesInstalledLock = new object(); + + #endregion Private Fields + } +} diff --git a/Libraries/ImeSharp/TextStore.cs b/Libraries/ImeSharp/TextStore.cs new file mode 100644 index 000000000..b7bf9cf87 --- /dev/null +++ b/Libraries/ImeSharp/TextStore.cs @@ -0,0 +1,963 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using ImeSharp.Native; +using SharpGen.Runtime; +using SharpGen.Runtime.Win32; +using TsfSharp; + +namespace ImeSharp +{ + internal class TextStore : CallbackBase, + ITextStoreACP, + ITfContextOwnerCompositionSink, + ITfTextEditSink, + ITfUIElementSink + { + public static readonly Guid IID_ITextStoreACPSink = new Guid(0x22d44c94, 0xa419, 0x4542, 0xa2, 0x72, 0xae, 0x26, 0x09, 0x3e, 0xce, 0xcf); + public static readonly Guid GUID_PROP_COMPOSING = new Guid("e12ac060-af15-11d2-afc5-00105a2799b5"); + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + // Creates a TextStore instance. + public TextStore(IntPtr windowHandle) + { + _windowHandle = windowHandle; + + + _viewCookie = Environment.TickCount; + + _editCookie = Tsf.TF_INVALID_COOKIE; + _uiElementSinkCookie = Tsf.TF_INVALID_COOKIE; + _textEditSinkCookie = Tsf.TF_INVALID_COOKIE; + + _IMEStringPool = ArrayPool.Shared; + } + + #endregion Constructors + + //------------------------------------------------------ + // + // Methods - ITextStoreACP + // + //------------------------------------------------------ + + #region ITextStoreACP + + public void AdviseSink(Guid riid, IUnknown obj, int flags) + { + ITextStoreACPSink sink; + + if (riid != IID_ITextStoreACPSink) + throw new COMException("TextStore_CONNECT_E_CANNOTCONNECT"); + + sink = (obj as ComObject).QueryInterface(); + + if (sink == null) + throw new COMException("TextStore_E_NOINTERFACE"); + + // It's legal to replace existing sink. + if (_sink != null) + _sink.Dispose(); + + (obj as ComObject).Dispose(); + + _sink = sink; + } + + public void UnadviseSink(IUnknown obj) + { + var sink = (obj as ComObject).QueryInterface(); + if (sink.NativePointer != _sink.NativePointer) + throw new COMException("TextStore_CONNECT_E_NOCONNECTION"); + + _sink.Release(); + _sink = null; + } + + private bool _LockDocument(TsfSharp.TsLfFlags dwLockFlags) + { + if (_locked) + return false; + + _locked = true; + _lockFlags = dwLockFlags; + + return true; + } + + private void ResetIfRequired() + { + if (!_commited) + return; + + _commited = false; + + TsTextchange textChange; + textChange.AcpStart = 0; + textChange.AcpOldEnd = _inputBuffer.Count; + textChange.AcpNewEnd = 0; + _inputBuffer.Clear(); + + _sink.OnTextChange(0, textChange); + + _acpStart = _acpEnd = 0; + _sink.OnSelectionChange(); + _commitStart = _commitLength = 0; + + //Debug.WriteLine("TextStore reset!!!"); + } + + private void _UnlockDocument() + { + Result hr; + _locked = false; + _lockFlags = 0; + + ResetIfRequired(); + + //if there is a queued lock, grant it + if (_lockRequestQueue.Count > 0) + { + hr = RequestLock(_lockRequestQueue.Dequeue()); + } + + //if any layout changes occurred during the lock, notify the manager + if (_layoutChanged) + { + _layoutChanged = false; + _sink.OnLayoutChange(TsLayoutCode.TsLcChange, _viewCookie); + } + } + + private bool _IsLocked(TsfSharp.TsLfFlags dwLockType) + { + return _locked && (_lockFlags & dwLockType) != 0; + } + + public Result RequestLock(TsfSharp.TsLfFlags dwLockFlags) + { + Result hrSession; + + if (_sink == null) + throw new COMException("TextStore_NoSink"); + + if (dwLockFlags == 0) + throw new COMException("TextStore_BadLockFlags"); + + hrSession = Result.Fail; + + if (_locked) + { + //the document is locked + + if ((dwLockFlags & TsfSharp.TsLfFlags.Sync) == TsfSharp.TsLfFlags.Sync) + { + /* + The caller wants an immediate lock, but this cannot be granted because + the document is already locked. + */ + hrSession = (int)TsErrors.TsESynchronous; + } + else + { + //the request is asynchronous + + //Queue the lock request + _lockRequestQueue.Enqueue(dwLockFlags); + hrSession = (int)TsErrors.TsSAsync; + } + + return hrSession; + } + + //lock the document + _LockDocument(dwLockFlags); + + //call OnLockGranted + hrSession = _sink.OnLockGranted(dwLockFlags); + + //unlock the document + _UnlockDocument(); + + return hrSession; + } + + public TsStatus GetStatus() + { + TsStatus status = new TsStatus(); + status.DynamicFlags = 0; + status.StaticFlags = 0; + + return status; + } + + public void QueryInsert(int acpTestStart, int acpTestEnd, uint cch, out int acpResultStart, out int acpResultEnd) + { + acpResultStart = acpResultEnd = 0; + + // Fix possible crash + if (_inputBuffer.Count == 0) + return; + + //Queryins + if (acpTestStart > _inputBuffer.Count || acpTestEnd > _inputBuffer.Count) + throw new COMException("", Result.InvalidArg.Code); + + //Microsoft Pinyin seems does not init the result value, so we set the test value here, in case crash + acpResultStart = acpTestStart; + acpResultEnd = acpTestEnd; + } + + public uint GetSelection(uint index, ref TsSelectionAcp selection) + { + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + //return NativeMethods.TS_E_NOLOCK; + throw new COMException("", (int)TsErrors.TsENolock); + } + + //check the requested index + if (-1 == (int)index) + { + index = 0; + } + else if (index > 1) + { + /* + The index is too high. This app only supports one selection. + */ + throw new COMException("", Result.InvalidArg.Code); + } + + selection.AcpStart = _acpStart; + selection.AcpEnd = _acpEnd; + selection.Style.InterimCharFlag = _interimChar; + + if (_interimChar) + { + /* + fInterimChar will be set when an intermediate character has been + set. One example of when this will happen is when an IME is being + used to enter characters and a character has been set, but the IME + is still active. + */ + selection.Style.Ase = TsActiveSelEnd.TsAeNone; + } + else + { + selection.Style.Ase = _activeSelectionEnd; + } + + return 1; + } + + public void SetSelection(uint count, ref TsSelectionAcp selections) + { + //this implementaiton only supports a single selection + if (count != 1) + throw new COMException("", Result.InvalidArg.Code); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Readwrite)) + { + //the caller doesn't have a lock + //return NativeMethods.TS_E_NOLOCK; + throw new COMException("", (int)TsErrors.TsENolock); + } + + _acpStart = selections.AcpStart; + _acpEnd = selections.AcpEnd; + _interimChar = selections.Style.InterimCharFlag; + + if (_interimChar) + { + /* + fInterimChar will be set when an intermediate character has been + set. One example of when this will happen is when an IME is being + used to enter characters and a character has been set, but the IME + is still active. + */ + _activeSelectionEnd = TsActiveSelEnd.TsAeNone; + } + else + { + _activeSelectionEnd = selections.Style.Ase; + } + + //if the selection end is at the start of the selection, reverse the parameters + int lStart = _acpStart; + int lEnd = _acpEnd; + + if (TsActiveSelEnd.TsAeStart == _activeSelectionEnd) + { + lStart = _acpEnd; + lEnd = _acpStart; + } + } + + + public void GetText(int acpStart, int acpEnd, System.IntPtr pchPlain, uint cchPlainReq, out uint cchPlainRet, + ref TsfSharp.TsRuninfo rgRunInfo, uint cRunInfoReq, out uint cRunInfoRet, out int acpNext) + { + cchPlainRet = 0; + cRunInfoRet = 0; + acpNext = 0; + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + bool fDoText = cchPlainReq > 0; + bool fDoRunInfo = cRunInfoReq > 0; + int cchTotal; + + cchPlainRet = 0; + acpNext = acpStart; + + cchTotal = _inputBuffer.Count; + + //validate the start pos + if ((acpStart < 0) || (acpStart > cchTotal)) + { + throw new COMException("", Result.InvalidArg.Code); + } + else + { + //are we at the end of the document + if (acpStart == cchTotal) + { + return; + } + else + { + int cchReq; + + /* + acpEnd will be -1 if all of the text up to the end is being requested. + */ + + if (acpEnd >= acpStart) + { + cchReq = acpEnd - acpStart; + } + else + { + cchReq = cchTotal - acpStart; + } + + if (fDoText) + { + if (cchReq > cchPlainReq) + { + cchReq = (int)cchPlainReq; + } + + //extract the specified text range + if (pchPlain != IntPtr.Zero && cchPlainReq > 0) + { + //_inputBuffer.CopyTo(acpStart, pchPlain, 0, cchReq); + + unsafe + { + var ptr = (char*)pchPlain; + + for (int i = acpStart; i < cchReq; i++) + { + *ptr = _inputBuffer[i]; + ptr++; + } + } + } + } + + //it is possible that only the length of the text is being requested + cchPlainRet = (uint)cchReq; + + if (fDoRunInfo) + { + /* + Runs are used to separate text characters from formatting characters. + + In this example, sequences inside and including the <> are treated as + control sequences and are not displayed. + + Plain text = "Text formatting." + Actual text = "Text formatting." + + If all of this text were requested, the run sequence would look like this: + + prgRunInfo[0].type = TS_RT_PLAIN; //"Text " + prgRunInfo[0].uCount = 5; + + prgRunInfo[1].type = TS_RT_HIDDEN; // + prgRunInfo[1].uCount = 6; + + prgRunInfo[2].type = TS_RT_PLAIN; //"formatting" + prgRunInfo[2].uCount = 10; + + prgRunInfo[3].type = TS_RT_HIDDEN; // + prgRunInfo[3].uCount = 8; + + prgRunInfo[4].type = TS_RT_PLAIN; //"." + prgRunInfo[4].uCount = 1; + + TS_RT_OPAQUE is used to indicate characters or character sequences + that are in the document, but are used privately by the application + and do not map to text. Runs of text tagged with TS_RT_OPAQUE should + NOT be included in the pchPlain or cchPlainOut [out] parameters. + */ + + /* + This implementation is plain text, so the text only consists of one run. + If there were multiple runs, it would be an error to have consecuative runs + of the same type. + */ + rgRunInfo.Type = TsRunType.TsRtPlain; + rgRunInfo.Count = (uint)cchReq; + } + + acpNext = acpStart + cchReq; + } + } + } + + public TsTextchange SetText(int dwFlags, int acpStart, int acpEnd, string pchText, uint cch) + { + /* + dwFlags can be: + TS_ST_CORRECTION + */ + TsTextchange change = new TsTextchange(); + + //set the selection to the specified range + TsSelectionAcp tsa = new TsSelectionAcp(); + tsa.AcpStart = acpStart; + tsa.AcpEnd = acpEnd; + tsa.Style.Ase = TsActiveSelEnd.TsAeStart; + tsa.Style.InterimCharFlag = false; + + SetSelection(1, ref tsa); + + int start, end; + InsertTextAtSelection(TsIasFlags.Noquery, pchText, cch, out start, out end, out change); + + return change; + } + + public IDataObject GetFormattedText(int startIndex, int endIndex) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public IUnknown GetEmbedded(int index, Guid guidService, Guid riid) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public RawBool QueryInsertEmbedded(Guid guidService, ref Formatetc formatEtc) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public TsTextchange InsertEmbedded(int flags, int startIndex, int endIndex, TsfSharp.IDataObject dataObjectRef) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void InsertTextAtSelection(TsfSharp.TsIasFlags dwFlags, string pchText, uint cch, out int pacpStart, out int pacpEnd, out TsfSharp.TsTextchange pChange) + { + pacpStart = pacpEnd = 0; + pChange = new TsTextchange(); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Readwrite)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + int acpStart; + int acpOldEnd; + int acpNewEnd; + + acpOldEnd = _acpEnd; + + //set the start point after the insertion + acpStart = _acpStart; + + //set the end point after the insertion + acpNewEnd = _acpStart + (int)cch; + + if ((dwFlags & TsIasFlags.Queryonly) == TsIasFlags.Queryonly) + { + pacpStart = acpStart; + pacpEnd = acpOldEnd; + return; + } + + //insert the text + _inputBuffer.RemoveRange(acpStart, acpOldEnd - acpStart); + _inputBuffer.InsertRange(acpStart, pchText); + + //set the selection + _acpStart = acpStart; + _acpEnd = acpNewEnd; + + if ((dwFlags & TsIasFlags.Noquery) != TsIasFlags.Noquery) + { + pacpStart = acpStart; + pacpEnd = acpNewEnd; + } + + //set the TS_TEXTCHANGE members + pChange.AcpStart = acpStart; + pChange.AcpOldEnd = acpOldEnd; + pChange.AcpNewEnd = acpNewEnd; + + //defer the layout change notification until the document is unlocked + _layoutChanged = true; + } + + public void InsertEmbeddedAtSelection(int flags, IDataObject obj, out int startIndex, out int endIndex, out TsTextchange change) + { + startIndex = endIndex = 0; + change = new TsTextchange(); + throw new COMException("", Result.NotImplemented.Code); + } + + public void RequestSupportedAttrs(int flags, uint cFilterAttrs, ref Guid filterAttributes) + { + } + + public void RequestAttrsAtPosition(int index, uint cFilterAttrs, ref Guid filterAttributes, int flags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + + public void RequestAttrsTransitioningAtPosition(int position, uint cFilterAttrs, ref Guid filterAttributes, int flags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void FindNextAttrTransition(int startIndex, int haltIndex, uint cFilterAttrs, ref Guid filterAttributes, int flags, out int acpNext, out RawBool found, out int foundOffset) + { + acpNext = 0; + found = false; + foundOffset = 0; + } + + public uint RetrieveRequestedAttrs(uint ulCount, ref TsfSharp.TsAttrval aAttrValsRef) + { + return 0; + } + + public int GetEndACP() + { + int acp = 0; + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + acp = _inputBuffer.Count; + + return acp; + } + + public int GetActiveView() + { + return _viewCookie; + } + + public int GetACPFromPoint(int viewCookie, TsfSharp.Point tsfPoint, int dwFlags) + { + throw new COMException("", Result.NotImplemented.Code); + } + + public void GetTextExt(int viewCookie, int acpStart, int acpEnd, out Rect rect, out RawBool clipped) + { + clipped = false; + rect = InputMethod.TextInputRect; + + if (_viewCookie != viewCookie) + throw new COMException("", Result.InvalidArg.Code); + + //does the caller have a lock + if (!_IsLocked(TsLfFlags.Read)) + { + //the caller doesn't have a lock + throw new COMException("", (int)TsErrors.TsENolock); + } + + //According to Microsoft's doc, an ime should not make empty request, + //but some ime draw comp text themseleves, when empty req will be make + //Check empty request + //if (acpStart == acpEnd) { + // return E_INVALIDARG; + //} + + NativeMethods.MapWindowPoints(_windowHandle, IntPtr.Zero, ref rect, 2); + } + + public Rect GetScreenExt(int viewCookie) + { + Rect rect = new Rect(); + + if (_viewCookie != viewCookie) + throw new COMException("", Result.InvalidArg.Code); + + NativeMethods.GetWindowRect(_windowHandle, out rect); + + return rect; + } + + public IntPtr GetWnd(int viewCookie) + { + if (viewCookie != _viewCookie) + { + throw new COMException("", Result.False.Code); + } + + return _windowHandle; + } + + #endregion ITextStoreACP2 + + + //------------------------------------------------------ + // + // Public Methods - ITfContextOwnerCompositionSink + // + //------------------------------------------------------ + + #region ITfContextOwnerCompositionSink + + public RawBool OnStartComposition(ITfCompositionView view) + { + // Return true in ok to start the composition. + RawBool ok = true; + _compositionStart = _compositionLength = 0; + _currentComposition.Clear(); + + InputMethod.OnTextCompositionStarted(this); + _compViews.Add(view); + + return ok; + } + + public void OnUpdateComposition(ITfCompositionView view, ITfRange rangeNew) + { + var range = view.Range; + var rangeacp = range.QueryInterface(); + + rangeacp.GetExtent(out _compositionStart, out _compositionLength); + rangeacp.Dispose(); + range.Dispose(); + _compViews.Add(view); + } + + public void OnEndComposition(ITfCompositionView view) + { + var range = view.Range; + var rangeacp = range.QueryInterface(); + + rangeacp.GetExtent(out _commitStart, out _commitLength); + rangeacp.Dispose(); + range.Dispose(); + + // Ensure composition string reset + _compositionStart = _compositionLength = 0; + _currentComposition.Clear(); + + InputMethod.ClearCandidates(); + InputMethod.OnTextCompositionEnded(this); + view.Dispose(); + foreach(var item in _compViews) + item.Dispose(); + _compViews.Clear(); + } + + #endregion ITfContextOwnerCompositionSink + + #region ITfTextEditSink + + public void OnEndEdit(ITfContext context, int ecReadOnly, ITfEditRecord editRecord) + { + ITfProperty property = context.GetProperty(GUID_PROP_COMPOSING); + + ITfRangeACP rangeACP = TextServicesContext.Current.ContextOwnerServices.CreateRange(_compositionStart, _compositionStart + _compositionLength); + Variant val = property.GetValue(ecReadOnly, rangeACP); + property.Dispose(); + rangeACP.Dispose(); + if (val.Value == null || (int)val.Value == 0) + { + if (_commitLength == 0 || _inputBuffer.Count == 0) + return; + + //Debug.WriteLine("Composition result: {0}", new object[] { new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray()) }); + + _commited = true; + for (int i = 0; i < _commitLength; i++) + InputMethod.OnTextCompositionResult(this, new string(_inputBuffer.GetRange(_commitStart, _commitLength).ToArray())); + } + + if (_commited) + return; + + if (_inputBuffer.Count == 0 && _compositionLength > 0) // Composition just ended + return; + + _currentComposition.Clear(); + for (int i = 0; i < _compositionLength; i++) + _currentComposition.Add(_inputBuffer[_compositionStart + i]); + + InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd); + + //var compStr = new string(_currentComposition.ToArray()); + //compStr = compStr.Insert(_acpEnd, "|"); + //Debug.WriteLine("Composition string: {0}, cursor pos: {1}", compStr, _acpEnd); + } + + #endregion ITfTextEditSink + + //------------------------------------------------------ + // + // Public Methods - ITfUIElementSink + // + //------------------------------------------------------ + + #region ITfUIElementSink + + public RawBool BeginUIElement(int dwUIElementId) + { + // Hide OS rendered Candidate list Window + RawBool pbShow = InputMethod.ShowOSImeWindow; + + OnUIElement(dwUIElementId, true); + + return pbShow; + } + + public void UpdateUIElement(int dwUIElementId) + { + OnUIElement(dwUIElementId, false); + } + + public void EndUIElement(int dwUIElementId) + { + } + + private void OnUIElement(int uiElementId, bool onStart) + { + if (InputMethod.ShowOSImeWindow || !_supportUIElement) return; + + ITfUIElement uiElement = TextServicesContext.Current.UIElementMgr.GetUIElement(uiElementId); + + ITfCandidateListUIElementBehavior candList; + + try + { + candList = uiElement.QueryInterface(); + } + catch (SharpGenException) + { + _supportUIElement = false; + return; + } + finally + { + uiElement.Dispose(); + } + + uint selection = 0; + uint currentPage = 0; + uint count = 0; + uint pageCount = 0; + uint pageStart = 0; + uint pageSize = 0; + uint i, j; + + selection = candList.GetSelection(); + currentPage = candList.GetCurrentPage(); + + count = candList.GetCount(); + + pageCount = candList.GetPageIndex(null, 0); + + if (pageCount > 0) + { + uint[] pageStartIndexes = ArrayPool.Shared.Rent((int)pageCount); + pageCount = candList.GetPageIndex(pageStartIndexes, pageCount); + pageStart = pageStartIndexes[currentPage]; + + if (pageStart >= count - 1) + { + candList.Abort(); + ArrayPool.Shared.Return(pageStartIndexes); + return; + } + + if (currentPage < pageCount - 1) + pageSize = Math.Min(count, pageStartIndexes[currentPage + 1]) - pageStart; + else + pageSize = count - pageStart; + + ArrayPool.Shared.Return(pageStartIndexes); + } + + selection -= pageStart; + + IMEString[] candidates = _IMEStringPool.Rent((int)pageSize); + + IntPtr bStrPtr; + for (i = pageStart, j = 0; i < count && j < pageSize; i++, j++) + { + bStrPtr = candList.GetString(i); + candidates[j] = new IMEString(bStrPtr); + } + + //Debug.WriteLine("TSF========TSF"); + //Debug.WriteLine("pageStart: {0}, pageSize: {1}, selection: {2}, currentPage: {3} candidates:", pageStart, pageSize, selection, currentPage); + //for (int k = 0; k < candidates.Length; k++) + // Debug.WriteLine(" {2}{0}.{1}", k + 1, candidates[k], k == selection ? "*" : ""); + //Debug.WriteLine("TSF++++++++TSF"); + + InputMethod.CandidatePageStart = (int)pageStart; + InputMethod.CandidatePageSize = (int)pageSize; + InputMethod.CandidateSelection = (int)selection; + InputMethod.CandidateList = candidates; + + if (_currentComposition != null) + { + InputMethod.OnTextComposition(this, new IMEString(_currentComposition), _acpEnd); + _IMEStringPool.Return(candidates); + } + + candList.Dispose(); + } + + #endregion ITfUIElementSink + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + public static TextStore Current + { + get + { + TextStore defaultTextStore = InputMethod.DefaultTextStore; + if (defaultTextStore == null) + { + defaultTextStore = InputMethod.DefaultTextStore = new TextStore(InputMethod.WindowHandle); + + defaultTextStore.Register(); + } + + return defaultTextStore; + } + } + + public ITfDocumentMgr DocumentManager + { + get { return _documentMgr; } + set { _documentMgr = value; } + } + + // EditCookie for ITfContext. + public int EditCookie + { + // get { return _editCookie; } + set { _editCookie = value; } + } + + public int UIElementSinkCookie + { + get { return _uiElementSinkCookie; } + set { _uiElementSinkCookie = value; } + } + + public int TextEditSinkCookie + { + get { return _textEditSinkCookie; } + set { _textEditSinkCookie = value; } + } + + public bool SupportUIElement { get { return _supportUIElement; } } + + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + // This function calls TextServicesContext to create TSF document and start transitory extension. + private void Register() + { + // Create TSF document and advise the sink to it. + TextServicesContext.Current.RegisterTextStore(this); + } + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + // The TSF document object. This is a native resource. + private ITfDocumentMgr _documentMgr; + + private int _viewCookie; + + // The edit cookie TSF returns from CreateContext. + private int _editCookie; + private int _uiElementSinkCookie; + private int _textEditSinkCookie; + + private ITextStoreACPSink _sink; + private IntPtr _windowHandle; + private int _acpStart; + private int _acpEnd; + private bool _interimChar; + private TsActiveSelEnd _activeSelectionEnd; + private List _inputBuffer = new List(); + + private bool _locked; + private TsLfFlags _lockFlags; + private Queue _lockRequestQueue = new Queue(); + private bool _layoutChanged; + + private List _currentComposition = new List(); + private int _compositionStart; + private int _compositionLength; + private int _commitStart; + private int _commitLength; + private bool _commited; + + private bool _supportUIElement = true; + private List _compViews = new List(); + + private ArrayPool _IMEStringPool; + + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs index ec4d56407..8b166be16 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs @@ -106,6 +106,11 @@ namespace Microsoft.Xna.Framework { /// This event is only supported on the Windows DirectX, Windows OpenGL and Linux platforms. /// public event EventHandler TextInput; + + /// + /// Used for displaying uncommitted IME text. + /// + public event EventHandler TextEditing; #endif #endregion Events @@ -152,6 +157,11 @@ namespace Microsoft.Xna.Framework { { EventHelpers.Raise(this, TextInput, e); } + + protected void OnTextEditing(object sender, TextEditingEventArgs e) + { + EventHelpers.Raise(this, TextEditing, e); + } #endif protected internal abstract void SetSupportedOrientations (DisplayOrientation orientations); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj index 5f9dbc68a..41c2f71d8 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj index dd5a32d33..a8b412890 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.MacOS.NetStandard.csproj @@ -71,6 +71,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index 3ff6019b1..e6bc855bf 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -84,6 +84,8 @@ + + Angle,Linux,MacOS,Windows,WindowsGL,WindowsUniversal diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 5ee24f7f0..8b4672ef6 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -240,6 +240,22 @@ internal static class Sdl return pointer; } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate bool d_sdl_istextinputactive(); + public static d_sdl_istextinputactive SDL_IsTextInputActive = FuncLoader.LoadFunction(NativeLibrary, "SDL_IsTextInputActive"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_starttextinput(); + public static d_sdl_starttextinput SDL_StartTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StartTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_stoptextinput(); + public static d_sdl_stoptextinput SDL_StopTextInput = FuncLoader.LoadFunction(NativeLibrary, "SDL_StopTextInput"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_settextinputrect(ref Rectangle rect); + public static d_sdl_settextinputrect SDL_SetTextInputRect = FuncLoader.LoadFunction(NativeLibrary, "SDL_SetTextInputRect"); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void d_sdl_clearerror(); public static d_sdl_clearerror ClearError = FuncLoader.LoadFunction(NativeLibrary, "SDL_ClearError"); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs index e18d8a4d4..fb2f05833 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -136,8 +137,7 @@ namespace Microsoft.Xna.Framework { var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); - if (!_keys.Contains(key)) - _keys.Add(key); + if (!_keys.Contains(key)) _keys.Add(key); //TODO: rethink all of this char character = (char)KeyboardUtil.ApplyModifiers(ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); @@ -161,24 +161,23 @@ namespace Microsoft.Xna.Framework var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); _keys.Remove(key); } + else if (ev.Type == Sdl.EventType.TextEditing) + { + string text; + unsafe { text = ReadString(ev.Text.Text); } + + _view.CallTextEditing(text, ev.Edit.Start, ev.Edit.Length); + } else if (ev.Type == Sdl.EventType.TextInput) { - int len = 0; - string text = String.Empty; - unsafe + string text; + unsafe { text = ReadString(ev.Text.Text); } + + if (text.Length is 0) { continue; } + + foreach (char c in text) { - while (Marshal.ReadByte ((IntPtr)ev.Text.Text, len) != 0) { - len++; - } - var buffer = new byte [len]; - Marshal.Copy ((IntPtr)ev.Text.Text, buffer, 0, len); - text = System.Text.Encoding.UTF8.GetString (buffer); - } - if (text.Length == 0) - continue; - foreach (var c in text) - { - var key = KeyboardUtil.ToXna((int)c); + var key = KeyboardUtil.ToXna(c); _view.CallTextInput(c, key); } } @@ -194,11 +193,22 @@ namespace Microsoft.Xna.Framework IsActive = false; else if (ev.Window.EventID == Sdl.Window.EventId.Moved) _view.Moved(); - else if (ev.Window.EventID == Sdl.Window.EventId.Close) - _isExiting++; + else if (ev.Window.EventID == Sdl.Window.EventId.Close) _isExiting++; } } } + + static unsafe string ReadString(byte* ptr) + { + int len = 0; + while (Marshal.ReadByte((IntPtr)ptr, len) != 0) + { + len++; + } + var buffer = new byte [len]; + Marshal.Copy((IntPtr)ptr, buffer, 0, len); + return Encoding.UTF8.GetString(buffer); + } } public override void StartRunLoop() diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs index 7d5a071ca..91eab7e6a 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs @@ -113,6 +113,12 @@ namespace Microsoft.Xna.Framework Sdl.SetHint("SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0"); Sdl.SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1"); + /* + * By default SDL2 will hide IME popups since it probably assumes the game will implement their own suggestions box. + * We don't want that, so this hint will allow the system native IME popups to show up when typing in the game. + */ + Sdl.SetHint("SDL_HINT_IME_SHOW_UI", "1"); + // when running NUnit tests entry assembly can be null if (Assembly.GetEntryAssembly() != null) { @@ -333,6 +339,11 @@ namespace Microsoft.Xna.Framework OnTextInput(this, new TextInputEventArgs(c, key)); } + public void CallTextEditing(string text, int start, int length) + { + OnTextEditing(this, new TextEditingEventArgs(text, start, length)); + } + public void DropFile(string filePath) { OnFileDropped(new FileDropEventArgs(filePath)); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs new file mode 100644 index 000000000..cce9ccc66 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextEditingEventArgs.cs @@ -0,0 +1,17 @@ +using System; +namespace Microsoft.Xna.Framework +{ + public sealed class TextEditingEventArgs : EventArgs + { + public readonly string Text; + public readonly int Start; + public readonly int Length; + + public TextEditingEventArgs(string text, int start, int length) + { + Text = text; + Start = start; + Length = length; + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs new file mode 100644 index 000000000..df18c7bba --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/TextInput.cs @@ -0,0 +1,29 @@ +#nullable enable + +namespace Microsoft.Xna.Framework +{ + public static class TextInput + { + public static void StartTextInput() + { + Sdl.SDL_StartTextInput(); + } + + public static void StopTextInput() + { + Sdl.SDL_StopTextInput(); + } + + public static void SetTextInputRect(Rectangle rectangle) + { + Sdl.Rectangle r = new Sdl.Rectangle + { + X = rectangle.X, + Y = rectangle.Y, + Width = rectangle.Width, + Height = rectangle.Height + }; + Sdl.SDL_SetTextInputRect(ref r); + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index bdd69273a..846fdc9a0 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x64/libSDL2-2.0.so.0 differ diff --git a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 old mode 100644 new mode 100755 index 1f9bf7bb4..33ae87596 Binary files a/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 and b/Libraries/MonoGame.Framework/Src/ThirdParty/Dependencies/SDL/Linux/x86/libSDL2-2.0.so.0 differ diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 6d0f82d4f..8c10904cd 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,11 +39,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxServer", "Barotrauma\B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.NetStandard", "Libraries\MonoGame.Framework\Src\MonoGame.Framework\MonoGame.Framework.Linux.NetStandard.csproj", "{33E95A21-E071-4432-819F-AA64CF3EF3F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{60B82E13-2CDD-4C74-8373-FD7264D6C80B}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{95c4d59d-9be4-4278-b4f8-46c0ba1a3916}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/MacSolution.sln b/MacSolution.sln index 77418fa58..ee5cddf3f 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,11 +39,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.MacOS.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.Posix", "Libraries\Facepunch.Steamworks\Facepunch.Steamworks.Posix.csproj", "{F10CE3BB-26B8-446E-84D2-86D25E850F61}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{36B38D18-3574-4B67-A89C-FD3C2D39F1D6}" EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Libraries\GameAnalytics\GA-SDK-MONO-SHARED\GA-SDK-MONO-SHARED.projitems*{c54f0dfe-add3-4767-8cbc-101859218d66}*SharedItemsImports = 5 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 diff --git a/WindowsSolution.sln b/WindowsSolution.sln index 54c1ad98e..d66f0eaf5 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29201.188 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D32A29D8-AC7B-4189-B734-8ED9EB4120D0}" ProjectSection(SolutionItems) = preProject @@ -39,9 +39,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XNATypes", "Libraries\XNATy EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Libraries\SharpFont\Source\SharpFont\SharpFont.NetStandard.csproj", "{6911872D-40EF-400C-B0A1-9985A19ED488}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAll", "Deploy\DeployAll\DeployAll.csproj", "{C98FE0D0-BC7D-4806-B592-734B53016FD8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImeSharp", "Libraries\ImeSharp\ImeSharp.csproj", "{D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -131,6 +133,12 @@ Global {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Release|x64.Build.0 = Release|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.ActiveCfg = Debug|Any CPU {C98FE0D0-BC7D-4806-B592-734B53016FD8}.Unstable|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Debug|x64.Build.0 = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.ActiveCfg = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Release|x64.Build.0 = Release|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.ActiveCfg = Debug|Any CPU + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6}.Unstable|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -150,6 +158,7 @@ Global {6911872D-40EF-400C-B0A1-9985A19ED488} = {DE36F45F-F09E-4719-B953-00D148F7722A} {C7212AE2-A925-4225-A639-AE0653EF65B0} = {78A9F0AA-5519-407A-9B72-2A09F5DF7068} {C98FE0D0-BC7D-4806-B592-734B53016FD8} = {F35DF9BF-0BED-4FEF-A51C-DD83C531882F} + {D42D8E48-A687-445D-AAF1-9E7E6EBF1DE6} = {DE36F45F-F09E-4719-B953-00D148F7722A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17032EAB-554B-4B44-A4F6-EFB177ACAB7A}