diff --git a/.gitignore b/.gitignore index cb34e1f65..4dd2d8677 100644 --- a/.gitignore +++ b/.gitignore @@ -40,9 +40,5 @@ Libraries/webm_mem_playback/opus_x64_linux/ # Win desktop.ini -# Merge script +#Merge script temp.txt - -# Private assets -Barotrauma/BarotraumaShared/Content/* -.github/ISSUE_TEMPLATE/release-checklist.md diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 7d612effb..e01494501 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -9,7 +9,7 @@ namespace Barotrauma { public override void DebugDraw(SpriteBatch spriteBatch) { - if (Character.IsDead) return; + if (Character.IsUnconscious || !Character.Enabled || !Enabled) { return; } Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; @@ -38,7 +38,7 @@ namespace Barotrauma } targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); - if (wallTarget != null && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) + if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs new file mode 100644 index 000000000..db9309bbe --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs @@ -0,0 +1,32 @@ +namespace Barotrauma +{ + abstract partial class AIObjective + { + public static Sprite GetSprite(string identifier, string option, Entity targetEntity) + { + if (string.IsNullOrEmpty(identifier)) + { + return null; + } + identifier = identifier.RemoveWhitespace(); + if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + { + if (!string.IsNullOrEmpty(option) && orderPrefab.OptionSprites.TryGetValue(option, out var optionSprite)) + { + return optionSprite; + } + if (targetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) + { + return targetItem.Prefab.MinimapIcon; + } + return orderPrefab.SymbolSprite; + } + return GUI.Style.GetComponentStyle($"{identifier}objectiveicon")?.GetDefaultSprite(); + } + + public Sprite GetSprite() + { + return GetSprite(Identifier, Option, (this as AIObjectiveOperateItem)?.OperateTarget); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index ad368d0aa..e994f5954 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -39,10 +39,7 @@ namespace Barotrauma partial void DamageParticles(float deltaTime, Vector2 worldPosition) { - if (particleEmitter != null) - { - particleEmitter.Emit(deltaTime, worldPosition); - } + particleEmitter?.Emit(deltaTime, worldPosition); if (sound != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 20feee440..8eca0c227 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -102,11 +102,13 @@ namespace Barotrauma set { chromaticAberrationStrength = MathHelper.Clamp(value, 0.0f, 100.0f); } } + public Color GrainColor { get; set; } + private float grainStrength; public float GrainStrength { get => grainStrength; - set => grainStrength = MathHelper.Clamp(value, 0.0f, 1.0f); + set => grainStrength = Math.Max(0, value); } private readonly List bloodEmitters = new List(); @@ -199,7 +201,7 @@ namespace Barotrauma /// public void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam = true) { - if (DisableControls || GUI.PauseMenuOpen || GUI.SettingsMenuOpen) + if (DisableControls || GUI.InputBlockingMenuOpen) { foreach (Key key in keys) { @@ -321,7 +323,7 @@ namespace Barotrauma DoInteractionUpdate(deltaTime, mouseSimPos); } - if (!GUI.PauseMenuOpen && !GUI.SettingsMenuOpen) + if (!GUI.InputBlockingMenuOpen) { if (SelectedConstruction != null && (SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null && HUD.CloseHUD(ic.GuiFrame.Rect)) || @@ -505,14 +507,16 @@ namespace Barotrauma { continue; } - if (item.body != null && !item.body.Enabled) continue; - if (item.ParentInventory != null) continue; - if (ignoredItems != null && ignoredItems.Contains(item)) continue; + if (item.body != null && !item.body.Enabled) { continue; } + if (item.ParentInventory != null) { continue; } + if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } + if (item.Prefab.RequireCampaignInteract && item.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } if (draggingItemToWorld) { if (item.OwnInventory == null || + !item.OwnInventory.Container.AllowDragAndDrop || !item.OwnInventory.CanBePut(CharacterInventory.DraggingItems.First()) || !CanAccessInventory(item.OwnInventory)) { @@ -677,7 +681,7 @@ namespace Barotrauma else { //Ideally it shouldn't send the character entirely if we can't see them but /shrug, this isn't the most hacker-proof game atm - hudInfoVisible = controlled.CanSeeCharacter(this, controlled.ViewTarget == null ? controlled.WorldPosition : controlled.ViewTarget.WorldPosition); + hudInfoVisible = controlled.CanSeeTarget(this, controlled.ViewTarget); } hudInfoTimer = Rand.Range(0.5f, 1.0f); } @@ -859,7 +863,14 @@ namespace Barotrauma Color nameColor = Color.White; if (Controlled != null && TeamID != Controlled.TeamID) { - nameColor = TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + if (TeamID == CharacterTeamType.FriendlyNPC) + { + nameColor = UniqueNameColor ?? Color.SkyBlue; + } + else + { + nameColor = GUI.Style.Red; + } } if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 7cb31a3a8..0223840df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -296,7 +296,7 @@ namespace Barotrauma float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) continue; GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUI.BrokenIcon, - Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); + Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) @@ -341,7 +341,7 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - bool shiftDown = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); + bool shiftDown = PlayerInput.IsShiftDown(); if (shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) { shouldRecreateHudTexts = true; @@ -391,7 +391,16 @@ namespace Barotrauma if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); - GUI.DrawIndicator(spriteBatch, npc.WorldPosition, cam, 500.0f, iconStyle.GetDefaultSprite(), iconStyle.Color); + GUI.DrawIndicator(spriteBatch, npc.WorldPosition, cam, npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, iconStyle.GetDefaultSprite(), iconStyle.Color); + } + + foreach (Item item in Item.ItemList) + { + if (item.IconStyle is null || item.Submarine != character.Submarine) { continue; } + if (Vector2.DistanceSquared(character.Position, item.Position) > 500f*500f) { continue; } + var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); + if (body != null && body.UserData as Item != item) { continue; } + GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Vector2(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 3f1b518ac..0798a00d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -291,8 +291,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - - int eventType = msg.ReadRangedInteger(0, 6); + int eventType = msg.ReadRangedInteger(0, 9); switch (eventType) { case 0: //NetEntityEvent.Type.InventoryState @@ -303,9 +302,9 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); //read anyway to prevent messing up reading the rest of the message - UInt16 lastEventID = msg.ReadUInt16(); - byte itemCount = msg.ReadByte(); - for (int i = 0; i < itemCount; i++) + _ = msg.ReadUInt16(); + byte inventoryItemCount = msg.ReadByte(); + for (int i = 0; i < inventoryItemCount; i++) { msg.ReadUInt16(); } @@ -354,56 +353,102 @@ namespace Barotrauma info?.SetSkillLevel(skillIdentifier, skillLevel, Position + Vector2.UnitY * 150.0f); } break; - case 4: //NetEntityEvent.Type.ExecuteAttack + case 4: // NetEntityEvent.Type.SetAttackTarget + case 5: //NetEntityEvent.Type.ExecuteAttack int attackLimbIndex = msg.ReadByte(); UInt16 targetEntityID = msg.ReadUInt16(); int targetLimbIndex = msg.ReadByte(); - + Vector2 targetSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); //255 = entity already removed, no need to do anything if (attackLimbIndex == 255 || Removed) { break; } - if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; Limb targetLimb = null; if (!(FindEntityByID(targetEntityID) is IDamageable targetEntity)) { - DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target entity not found (ID {targetEntityID})"); + DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Target entity not found (ID {targetEntityID})"); break; } if (targetEntity is Character targetCharacter) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + DebugConsole.ThrowError($"Received invalid SetAttack/ExecuteAttack message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; } if (attackLimb?.attack != null) { - attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + if (eventType == 4) + { + SetAttackTarget(attackLimb, targetEntity, targetSimPos); + } + else + { + attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + } } break; - case 5: //NetEntityEvent.Type.AssignCampaignInteraction + case 6: //NetEntityEvent.Type.AssignCampaignInteraction byte campaignInteractionType = msg.ReadByte(); + bool requireConsciousness = msg.ReadBoolean(); (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); + RequireConsciousnessForCustomInteract = requireConsciousness; break; - case 6: //NetEntityEvent.Type.ObjectiveManagerOrderState - bool properData = msg.ReadBoolean(); - if (!properData) { break; } - int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); - var orderPrefab = Order.PrefabList[orderIndex]; - string option = null; - if (orderPrefab.HasOptions) + case 7: //NetEntityEvent.Type.ObjectiveManagerState + // 1 = order, 2 = objective + int msgType = msg.ReadRangedInteger(0, 2); + if (msgType == 0) { break; } + bool validData = msg.ReadBoolean(); + if (!validData) { break; } + if (msgType == 1) { - int optionIndex = msg.ReadRangedInteger(0, orderPrefab.Options.Length); - option = orderPrefab.Options[optionIndex]; + int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); + var orderPrefab = Order.PrefabList[orderIndex]; + string option = null; + if (orderPrefab.HasOptions) + { + int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); + if (optionIndex > -1) + { + option = orderPrefab.AllOptions[optionIndex]; + } + } + GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option); + } + else if (msgType == 2) + { + string identifier = msg.ReadString(); + string option = msg.ReadString(); + ushort objectiveTargetEntityId = msg.ReadUInt16(); + var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); + GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); + } + break; + case 8: //NetEntityEvent.Type.TeamChange + byte newTeamId = msg.ReadByte(); + ChangeTeam((CharacterTeamType)newTeamId); + break; + case 9: //NetEntityEvent.Type.AddToCrew + GameMain.GameSession.CrewManager.AddCharacter(this); + CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + ushort itemID = msg.ReadUInt16(); + if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } + item.AllowStealing = true; + var wifiComponent = item.GetComponent(); + if (wifiComponent != null) + { + wifiComponent.TeamID = teamID; + } } - GameMain.GameSession.CrewManager.SetHighlightedOrderIcon(this, orderPrefab.Identifier, option); break; } msg.ReadPadBits(); @@ -415,7 +460,7 @@ namespace Barotrauma { DebugConsole.Log("Reading character spawn data"); - if (GameMain.Client == null) return null; + if (GameMain.Client == null) { return null; } bool noInfo = inc.ReadBoolean(); ushort id = inc.ReadUInt16(); @@ -431,7 +476,15 @@ namespace Barotrauma Character character = null; if (noInfo) { - character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false); + try + { + character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e); + throw; + } bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) { @@ -447,8 +500,15 @@ namespace Barotrauma string infoSpeciesName = inc.ReadString(); CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - - character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); + try + { + character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e); + throw; + } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); if (character.CampaignInteractionType != CampaignMode.InteractionType.None) @@ -471,7 +531,7 @@ namespace Barotrauma var x = inc.ReadSingle(); var y = inc.ReadSingle(); var hull = FindEntityByID(inc.ReadUInt16()) as Hull; - targetPosition = new OrderTarget(new Vector2(x, y), hull, true); + targetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); } if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) @@ -485,7 +545,7 @@ namespace Barotrauma new Order(orderPrefab, targetPosition, orderGiver: orderGiver); character.SetOrder(order, orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, - orderPriority, orderGiver, speak: false); + orderPriority, orderGiver, speak: false, force: true); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 8bcb7e503..7ee5b8499 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -63,6 +63,8 @@ namespace Barotrauma private GUIListBox afflictionTooltip; + private static readonly Color oxygenLowGrainColor = new Color(0.1f, 0.1f, 0.1f, 1f); + private struct HeartratePosition { public float Time; @@ -671,17 +673,19 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { - var emitter = Character.BloodEmitters.FirstOrDefault(); - float particleMinScale = emitter != null ? emitter.Prefab.ScaleMin : 0.5f; - float particleMaxScale = emitter != null ? emitter.Prefab.ScaleMax : 1; + bool inWater = Character.AnimController.InWater; + var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; + var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab.DrawTarget == drawTarget || e.Prefab.ParticlePrefab.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); + float particleMinScale = emitter?.Prefab.Properties.ScaleMin ?? 0.5f; + float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); - bool inWater = Character.AnimController.InWater; if (!inWater) { bloodParticleSize *= 2.0f; } + // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); @@ -694,6 +698,12 @@ namespace Barotrauma } } + public static bool IsMouseOnHealthBar() + { + if (Character.Controlled?.CharacterHealth == null) { return false; } + return Character.Controlled.CharacterHealth.healthBar.State == GUIComponent.ComponentState.Hover; + } + public void UpdateHUD(float deltaTime) { if (GUI.DisableHUD) return; @@ -742,7 +752,9 @@ namespace Barotrauma float radialDistortStrength = 0.0f; float chromaticAberrationStrength = 0.0f; float grainStrength = 0.0f; + Color grainColor = Color.Transparent; + float oxygenLowStrength = 0.0f; if (Character.IsUnconscious) { blurStrength = 1.0f; @@ -750,10 +762,14 @@ namespace Barotrauma } else if (OxygenAmount < 100.0f) { - blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality); - distortStrength = blurStrength; - distortSpeed = (blurStrength + 1.0f); + oxygenLowStrength = Math.Min(1.0f - (OxygenAmount - LowOxygenThreshold) / LowOxygenThreshold, 1.0f); + blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality) * oxygenLowStrength; + distortStrength = blurStrength * oxygenLowStrength; + distortSpeed = blurStrength + 1.0f; distortSpeed *= distortSpeed * distortSpeed * distortSpeed; + + grainStrength = MathHelper.Lerp(0.5f, 10.0f, oxygenLowStrength); + grainColor = oxygenLowGrainColor; } foreach (Affliction affliction in afflictions) @@ -762,7 +778,12 @@ namespace Barotrauma blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); - grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); + float afflictionGrainStrength = affliction.GetScreenGrainStrength(); + if (afflictionGrainStrength > 0.0f) + { + grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); + grainColor = Color.Lerp(grainColor, Color.White, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); + } } foreach (LimbHealth limbHealth in limbHealths) { @@ -778,6 +799,7 @@ namespace Barotrauma Character.RadialDistortStrength = radialDistortStrength; Character.ChromaticAberrationStrength = chromaticAberrationStrength; Character.GrainStrength = grainStrength; + Character.GrainColor = grainColor; if (blurStrength > 0.0f) { distortTimer = (distortTimer + deltaTime * distortSpeed) % MathHelper.TwoPi; @@ -955,11 +977,9 @@ namespace Barotrauma highlightedLimbIndex = -1; } - Rectangle hoverArea = Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarArea); - healthBarHolder.CanBeFocused = healthBar.CanBeFocused = healthBarShadow.CanBeFocused = !Character.ShouldLockHud(); if (Character.AllowInput && UseHealthWindow && healthBar.Enabled && healthBar.CanBeFocused && - hoverArea.Contains(PlayerInput.MousePosition) && Inventory.SelectedSlot == null) + (GUI.IsMouseOn(healthBar) || highlightedAfflictionIcon != null) && Inventory.SelectedSlot == null) { healthBar.State = GUIComponent.ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -1076,8 +1096,11 @@ namespace Barotrauma DrawStatusHUD(spriteBatch); } + + private Pair highlightedAfflictionIcon = null; public void DrawStatusHUD(SpriteBatch spriteBatch) { + highlightedAfflictionIcon = null; //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { @@ -1092,7 +1115,6 @@ namespace Barotrauma statusIcons.Add(new Pair(affliction, affliction.Prefab.Name)); } - Pair highlightedIcon = null; Vector2 highlightedIconPos = Vector2.Zero; Rectangle afflictionArea = HUDLayoutSettings.AfflictionAreaLeft; @@ -1113,9 +1135,9 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab = affliction.Prefab; Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); - if (afflictionIconRect.Contains(PlayerInput.MousePosition) && !Character.ShouldLockHud()) + if (afflictionIconRect.Contains(PlayerInput.MousePosition) && !Character.ShouldLockHud() && GUI.MouseOn == null) { - highlightedIcon = statusIcon; + highlightedAfflictionIcon = statusIcon; highlightedIconPos = afflictionIconRect.Location.ToVector2(); } @@ -1135,7 +1157,7 @@ namespace Barotrauma highlightedIcon == statusIcon ? slot.HoverColor : slot.Color);*/ - float alphaMultiplier = highlightedIcon == statusIcon ? 1f : 0.8f; + float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; afflictionPrefab.Icon?.Draw(spriteBatch, pos.ToVector2(), @@ -1150,9 +1172,9 @@ namespace Barotrauma pos.Y += iconSize + (int)(5 * GUI.Scale); } - if (highlightedIcon != null) + if (highlightedAfflictionIcon != null) { - string nameTooltip = highlightedIcon.Second; + string nameTooltip = highlightedAfflictionIcon.Second; Vector2 offset = GUI.Font.MeasureString(nameTooltip); GUI.DrawString(spriteBatch, @@ -1315,7 +1337,7 @@ namespace Barotrauma child.Recalculate(); } - if (buttonToSelect != null) { buttonToSelect.OnClicked(buttonToSelect, "selectaffliction"); } + buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); afflictionIconContainer.RecalculateChildren(); @@ -2004,7 +2026,7 @@ namespace Barotrauma existingAffliction.PeriodicEffectTimers[periodicEffect.First] = periodicEffect.Second; foreach (StatusEffect effect in periodicEffect.First.StatusEffects) { - existingAffliction.ApplyStatusEffect(effect, deltaTime: 1.0f, this, targetLimb: null); + existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: null); } } } @@ -2071,7 +2093,7 @@ namespace Barotrauma foreach (StatusEffect effect in periodicEffect.First.StatusEffects) { Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(newAffliction.First)); - existingAffliction.ApplyStatusEffect(effect, deltaTime: 1.0f, this, targetLimb: targetLimb); + existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 01caadf07..f4c76ac1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -997,7 +997,7 @@ namespace Barotrauma } wearableColor = wearableItemComponent.Item.GetSpriteColor(); } - float textureScale = wearable.InheritTextureScale ? TextureScale : 1; + float textureScale = wearable.InheritTextureScale ? TextureScale : wearable.Scale; wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 1b8a94262..dea65b297 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -15,6 +15,7 @@ using Barotrauma.Extensions; using Barotrauma.Steam; using System.Threading.Tasks; using Barotrauma.MapCreatures.Behavior; +using static Barotrauma.FabricationRecipe; namespace Barotrauma { @@ -240,6 +241,7 @@ namespace Barotrauma case "toggleupperhud": case "togglecharacternames": case "fpscounter": + case "showperf": case "dumptofile": case "findentityids": case "setfreecamspeed": @@ -1367,6 +1369,241 @@ namespace Barotrauma } }, isCheat: false)); + commands.Add(new Command("analyzeitem", "analyzeitem: Analyzes one item for exploits.", (string[] args) => + { + if (args.Length < 1) return; + + List fabricableItems = new List(); + foreach (ItemPrefab iPrefab in ItemPrefab.Prefabs) + { + fabricableItems.AddRange(iPrefab.FabricationRecipes); + } + + string itemNameOrId = args[0].ToLowerInvariant(); + + ItemPrefab itemPrefab = + (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + + if (itemPrefab == null) + { + NewMessage("Item not found for analyzing."); + return; + } + NewMessage("Analyzing item " + itemPrefab.Name + " with base cost " + itemPrefab.DefaultPrice.Price); + + var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab); + // omega nesting incoming + if (fabricationRecipe != null) + { + foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + { + NewMessage(" If bought at " + itemLocationPrice.Key + " it costs " + itemLocationPrice.Value.Price); + int totalPrice = 0; + int? totalBestPrice = 0; + foreach (var ingredient in fabricationRecipe.RequiredItems) + { + foreach (ItemPrefab ingredientItemPrefab in ingredient.ItemPrefabs) + { + NewMessage(" Its ingredient " + ingredientItemPrefab.Name + " has base cost " + ingredientItemPrefab.DefaultPrice.Price); + totalPrice += ingredientItemPrefab.DefaultPrice.Price; + totalBestPrice += ingredientItemPrefab.GetMinPrice(); + int basePrice = ingredientItemPrefab.DefaultPrice.Price; + foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + { + if (basePrice > ingredientItemLocationPrice.Value.Price) + { + NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for cheaper, " + ingredientItemLocationPrice.Value.Price, Color.Yellow); + } + else + { + NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for more, " + ingredientItemLocationPrice.Value.Price, Color.Teal); + } + } + } + } + int costDifference = itemPrefab.DefaultPrice.Price - totalPrice; + NewMessage(" Constructing the item from store-bought items provides " + costDifference + " profit with default values."); + + if (totalBestPrice.HasValue) + { + int? bestDifference = itemLocationPrice.Value.Price - totalBestPrice; + NewMessage(" Constructing the item from store-bought items provides " + bestDifference + " profit with best-case scenario values."); + } + } + } + }, + () => + { + return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier)).ToArray() }; + }, isCheat: false)); + + commands.Add(new Command("checkcraftingexploits", "checkcraftingexploits: Finds outright item exploits created by buying store-bought ingredients and constructing them into sellable items.", (string[] args) => + { + List fabricableItems = new List(); + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + fabricableItems.AddRange(itemPrefab.FabricationRecipes); + } + List> costDifferences = new List>(); + + int maximumAllowedCost = 5; + + if (args.Length > 0) + { + Int32.TryParse(args[0], out maximumAllowedCost); + } + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + int? defaultCost = itemPrefab.DefaultPrice?.Price; + int? fabricationCostStore = null; + + var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab); + if (fabricationRecipe == null) + { + continue; + } + + bool canBeBought = true; + + foreach (var ingredient in fabricationRecipe.RequiredItems) + { + int? ingredientPrice = ingredient.ItemPrefabs.Where(p => p.CanBeBought).Min(ip => ip.DefaultPrice?.Price); + if (ingredientPrice.HasValue) + { + if (!fabricationCostStore.HasValue) { fabricationCostStore = 0; } + float useAmount = ingredient.UseCondition ? ingredient.MinCondition : 1.0f; + fabricationCostStore += (int)(ingredientPrice.Value * ingredient.Amount * useAmount); + } + else + { + canBeBought = false; + } + } + if (fabricationCostStore.HasValue && defaultCost.HasValue && canBeBought) + { + int costDifference = defaultCost.Value - fabricationCostStore.Value; + if (costDifference > maximumAllowedCost || costDifference < 0f) + { + float ratio = (float)fabricationCostStore.Value / defaultCost.Value; + string message = "Fabricating \"" + itemPrefab.Name + "\" costs " + (int)(ratio * 100) + "% of the price of the item, or " + costDifference + " more. Item price: " + defaultCost.Value + ", ingredient prices: " + fabricationCostStore.Value; + costDifferences.Add(new Tuple(message, costDifference)); + } + } + } + + costDifferences.Sort((x, y) => x.Item2.CompareTo(y.Item2)); + + foreach (Tuple costDifference in costDifferences) + { + Color color = Color.Yellow; + NewMessage(costDifference.Item1, color); + } + }, isCheat: false)); + + commands.Add(new Command("adjustprice", "adjustprice: Recursively prints out expected price adjustments for items derived from this item.", (string[] args) => + { + List fabricableItems = new List(); + foreach (ItemPrefab iP in ItemPrefab.Prefabs) + { + fabricableItems.AddRange(iP.FabricationRecipes); + } + if (args.Length < 2) + { + NewMessage("Item or value not defined."); + return; + } + string itemNameOrId = args[0].ToLowerInvariant(); + + ItemPrefab materialPrefab = + (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + + if (materialPrefab == null) + { + NewMessage("Item not found for price adjustment."); + return; + } + + AdjustItemTypes adjustItemType = AdjustItemTypes.NoAdjustment; + if (args.Length > 2) + { + switch (args[2].ToLowerInvariant()) + { + case "add": + adjustItemType = AdjustItemTypes.Additive; + break; + case "mult": + adjustItemType = AdjustItemTypes.Multiplicative; + break; + } + } + + if (Int32.TryParse(args[1].ToLowerInvariant(), out int newPrice)) + { + Dictionary newPrices = new Dictionary(); + PrintItemCosts(newPrices, materialPrefab, fabricableItems, newPrice, true, adjustItemType: adjustItemType); + PrintItemCosts(newPrices, materialPrefab, fabricableItems, newPrice, false, adjustItemType: adjustItemType); + } + + }, isCheat: false)); + + commands.Add(new Command("deconstructvalue", "deconstructvalue: Views and compares deconstructed component prices for this item.", (string[] args) => + { + List fabricableItems = new List(); + foreach (ItemPrefab iP in ItemPrefab.Prefabs) + { + fabricableItems.AddRange(iP.FabricationRecipes); + } + if (args.Length < 1) + { + NewMessage("Item not defined."); + return; + } + string itemNameOrId = args[0].ToLowerInvariant(); + + ItemPrefab parentItem = + (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + + if (parentItem == null) + { + NewMessage("Item not found for price adjustment."); + return; + } + + var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == parentItem); + int totalValue = 0; + NewMessage(parentItem.Name + " has the price " + parentItem.DefaultPrice.Price); + if (fabricationRecipe != null) + { + NewMessage(" It constructs from:"); + + foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems) + { + foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs) + { + NewMessage(" " + itemPrefab.Name + " has the price " + itemPrefab.DefaultPrice.Price); + totalValue += itemPrefab.DefaultPrice.Price; + } + } + NewMessage("Its total value was: " + totalValue); + totalValue = 0; + } + NewMessage(" The item deconstructs into:"); + foreach (DeconstructItem deconstructItem in parentItem.DeconstructItems) + { + ItemPrefab itemPrefab = + (MapEntityPrefab.Find(deconstructItem.ItemIdentifier, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + + NewMessage(" " + itemPrefab.Name + " has the price " + itemPrefab.DefaultPrice.Price); + totalValue += itemPrefab.DefaultPrice.Price; + } + NewMessage("Its deconstruct value was: " + totalValue); + + }, isCheat: false)); + commands.Add(new Command("setentityproperties", "setentityproperties [property name] [value]: Sets the value of some property on all selected items/structures in the sub editor.", (string[] args) => { if (args.Length != 2 || Screen.Selected != GameMain.SubEditorScreen) { return; } @@ -2935,5 +3172,155 @@ namespace Barotrauma return false; } } + + + private enum AdjustItemTypes + { + NoAdjustment, + Additive, + Multiplicative + } + + private static void PrintItemCosts(Dictionary newPrices, ItemPrefab materialPrefab, List fabricableItems, int newPrice, bool adjustDown, string depth = "", AdjustItemTypes adjustItemType = AdjustItemTypes.NoAdjustment) + { + if (newPrice < 1) + { + NewMessage(depth + materialPrefab.Name + " cannot be adjusted to this price, because it would become less than 1."); + return; + } + + depth += " "; + + if (newPrice > 0) + { + newPrices.TryAdd(materialPrefab, newPrice); + } + + int componentCost = 0; + int newComponentCost = 0; + + var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == materialPrefab); + + if (fabricationRecipe != null) + { + foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems) + { + foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs) + { + GetAdjustedPrice(itemPrefab, ref componentCost, ref newComponentCost, newPrices); + } + } + } + string componentCostMultiplier = ""; + if (componentCost > 0) + { + componentCostMultiplier = $" (Relative difference to component cost {GetComponentCostDifference(materialPrefab.DefaultPrice.Price, componentCost)} => {GetComponentCostDifference(newPrice, newComponentCost)}, or flat profit {(int)(materialPrefab.DefaultPrice.Price - (int)componentCost)} => {newPrice - newComponentCost})"; + } + string priceAdjustment = ""; + if (newPrice != materialPrefab.DefaultPrice.Price) + { + priceAdjustment = ", Suggested price adjustment is " + materialPrefab.DefaultPrice.Price + " => " + newPrice; + } + NewMessage(depth + materialPrefab.Name + "(" + materialPrefab.DefaultPrice.Price + ") " + priceAdjustment + componentCostMultiplier); + + if (adjustDown) + { + if (componentCost > 0) + { + double newPriceMult = (double)newPrice / (double)(materialPrefab.DefaultPrice.Price); + int newPriceDiff = componentCost + newPrice - materialPrefab.DefaultPrice.Price; + + switch (adjustItemType) + { + case AdjustItemTypes.Additive: + NewMessage(depth + materialPrefab.Name + "'s components should be adjusted " + componentCost + " => " + newPriceDiff); + break; + case AdjustItemTypes.Multiplicative: + NewMessage(depth + materialPrefab.Name + "'s components should be adjusted " + componentCost + " => " + Math.Round(newPriceMult * componentCost)); + break; + } + + if (fabricationRecipe != null) + { + foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems) + { + foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs) + { + if (itemPrefab.DefaultPrice != null) + { + switch (adjustItemType) + { + case AdjustItemTypes.NoAdjustment: + PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType); + break; + case AdjustItemTypes.Additive: + PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Count), adjustDown, depth, adjustItemType); + break; + case AdjustItemTypes.Multiplicative: + PrintItemCosts(newPrices, itemPrefab, fabricableItems, (int)(itemPrefab.DefaultPrice.Price * newPriceMult), adjustDown, depth, adjustItemType); + break; + } + } + } + } + } + } + } + else + { + var fabricationRecipes = fabricableItems.Where(f => f.RequiredItems.Any(x => x.ItemPrefabs.Contains(materialPrefab))); + + foreach (FabricationRecipe fabricationRecipeParent in fabricationRecipes) + { + if (fabricationRecipeParent.TargetItem.DefaultPrice != null) + { + int targetComponentCost = 0; + int newTargetComponentCost = 0; + + foreach (RequiredItem requiredItem in fabricationRecipeParent.RequiredItems) + { + foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs) + { + GetAdjustedPrice(itemPrefab, ref targetComponentCost, ref newTargetComponentCost, newPrices); + } + } + switch (adjustItemType) + { + case AdjustItemTypes.NoAdjustment: + PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, fabricationRecipeParent.TargetItem.DefaultPrice.Price, adjustDown, depth, adjustItemType); + break; + case AdjustItemTypes.Additive: + PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, fabricationRecipeParent.TargetItem.DefaultPrice.Price + newPrice - materialPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType); + break; + case AdjustItemTypes.Multiplicative: + double maintainedMultiplier = GetComponentCostDifference(fabricationRecipeParent.TargetItem.DefaultPrice.Price, targetComponentCost); + PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, (int)(newTargetComponentCost * maintainedMultiplier), adjustDown, depth, adjustItemType); + break; + } + } + } + } + } + + private static double GetComponentCostDifference(int itemCost, int componentCost) + { + return Math.Round((double)(itemCost / (double)componentCost), 2); + } + + private static void GetAdjustedPrice(ItemPrefab itemPrefab, ref int componentCost, ref int newComponentCost, Dictionary newPrices) + { + if (newPrices.TryGetValue(itemPrefab, out int newPrice)) + { + newComponentCost += newPrice; + } + else if (itemPrefab.DefaultPrice != null) + { + newComponentCost += itemPrefab.DefaultPrice.Price; + } + if (itemPrefab.DefaultPrice != null) + { + componentCost += itemPrefab.DefaultPrice.Price; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs b/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs index 8025acb22..f5d470f79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Decals/Decal.cs @@ -10,6 +10,8 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Hull hull, float depth) { + if (Sprite.Texture == null) { return; } + Vector2 drawPos = position + hull.Rect.Location.ToVector2(); if (hull.Submarine != null) { drawPos += hull.Submarine.DrawPosition; } drawPos.Y = -drawPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index fe31bb320..781d2c3ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -55,6 +55,12 @@ namespace Barotrauma { Debug.Assert(actionInstance == null || actionId == null); + if (GUI.InputBlockingMenuOpen) + { + if (actionId.HasValue) { SendIgnore(actionId.Value); } + return; + } + shouldFadeToBlack = fadeToBlack; if (lastMessageBox != null && !lastMessageBox.Closed && GUIMessageBox.MessageBoxes.Contains(lastMessageBox)) @@ -368,6 +374,15 @@ namespace Barotrauma GameMain.Client?.ClientPeer?.Send(outmsg, DeliveryMethod.Reliable); } + private static void SendIgnore(UInt16 actionId) + { + IWriteMessage outmsg = new WriteOnlyMessage(); + outmsg.Write((byte)ClientPacketHeader.EVENTMANAGER_RESPONSE); + outmsg.Write(actionId); + outmsg.Write(byte.MaxValue); + GameMain.Client?.ClientPeer?.Send(outmsg, DeliveryMethod.Reliable); + } + // Too broken, left it here if I ever want to come back to it private static List GetQuoteHighlights(string text, Color color) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 2d569af49..4d456d911 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -52,6 +52,7 @@ namespace Barotrauma GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "FloodingAmount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "FireAmount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "EnemyDanger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "MonsterTotalStrength: " + (int)Math.Round(monsterTotalStrength), Color.Lerp(GUI.Style.Green, GUI.Style.Red, monsterTotalStrength / 5000f), Color.Black * 0.6f, 0, GUI.SmallFont); #if DEBUG if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && @@ -75,7 +76,7 @@ namespace Barotrauma lastIntensityUpdate = (float) Timing.TotalTime; } - Rectangle graphRect = new Rectangle(15, y + 150, 150, 50); + Rectangle graphRect = new Rectangle(15, y + 165, 150, 50); GUI.DrawRectangle(spriteBatch, graphRect, Color.Black * 0.5f, true); intensityGraph.Draw(spriteBatch, graphRect, 1.0f, 0.0f, Color.Lerp(Color.White, GUI.Style.Red, currentIntensity)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index d4d0afdd6..55191b4b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -22,6 +22,13 @@ namespace Barotrauma public override void ClientReadInitial(IReadMessage msg) { + ushort targetItemCount = msg.ReadUInt16(); + for (int i = 0; i < targetItemCount; i++) + { + var item = Item.ReadSpawnData(msg); + items.Add(item); + } + byte characterCount = msg.ReadByte(); for (int i = 0; i < characterCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index 477cf476f..97bcbc4d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -1,9 +1,28 @@ using Barotrauma.Networking; +using System.Globalization; namespace Barotrauma { partial class CargoMission : Mission { + public override string GetMissionRewardText(Submarine sub) + { + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + + if (rewardPerCrate.HasValue) + { + string rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); + return TextManager.GetWithVariables("missionrewardcargopercrate", + new string[] { "[rewardpercrate]", "[itemcount]", "[maxitemcount]", "[totalreward]" }, + new string[] { rewardPerCrateText, itemsToSpawn.Count.ToString(), maxItemCount.ToString(), $"‖color:gui.orange‖{rewardText}‖end‖" }); + } + else + { + return TextManager.GetWithVariables("missionrewardcargo", + new string[] { "[totalreward]", "[itemcount]", "[maxitemcount]" }, + new string[] { $"‖color:gui.orange‖{rewardText}‖end‖", itemsToSpawn.Count.ToString(), maxItemCount.ToString() }); + } + } public override void ClientReadInitial(IReadMessage msg) { items.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs new file mode 100644 index 000000000..5fbdc15a5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs @@ -0,0 +1,37 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class EscortMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + byte characterCount = msg.ReadByte(); + + for (int i = 0; i < characterCount; i++) + { + Character character = Character.ReadSpawnData(msg); + characters.Add(character); + if (msg.ReadBoolean()) + { + terroristCharacters.Add(character); + } + ushort itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Item.ReadSpawnData(msg); + } + } + if (characters.Contains(null)) + { + throw new System.Exception("Error in EscortMission.ClientReadInitial: character list contains null (mission: " + Prefab.Identifier + ")"); + } + + if (characters.Count != characterCount) + { + throw new System.Exception("Error in EscortMission.ClientReadInitial: character count does not match the server count (" + characterCount + " != " + characters.Count + "mission: " + Prefab.Identifier + ")"); + } + InitCharacters(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 56e2e95f8..92d36b5fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -21,9 +21,9 @@ namespace Barotrauma return ToolBox.GradientLerp(t, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); } - public string GetMissionRewardText() + public virtual string GetMissionRewardText(Submarine sub) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)); + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); return TextManager.GetWithVariable("missionreward", "[reward]", $"‖color:gui.orange‖{rewardText}‖end‖"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs deleted file mode 100644 index e12fc3691..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/OutpostDestroyMission.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Barotrauma.Networking; - -namespace Barotrauma -{ - partial class OutpostDestroyMission : AbandonedOutpostMission - { - public override void ClientReadInitial(IReadMessage msg) - { - base.ClientReadInitial(msg); - ushort itemCount = msg.ReadUInt16(); - for (int i = 0; i < itemCount; i++) - { - var item = Item.ReadSpawnData(msg); - items.Add(item); - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..fa71317bc --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs @@ -0,0 +1,32 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class PirateMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + // duplicate code from escortmission, should possibly be combined, though additional loot items might be added so maybe not + byte characterCount = msg.ReadByte(); + + for (int i = 0; i < characterCount; i++) + { + characters.Add(Character.ReadSpawnData(msg)); + ushort itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Item.ReadSpawnData(msg); + } + } + if (characters.Contains(null)) + { + throw new System.Exception("Error in PirateMission.ClientReadInitial: character list contains null (mission: " + Prefab.Identifier + ")"); + } + + if (characters.Count != characterCount) + { + throw new System.Exception("Error in PirateMission.ClientReadInitial: character count does not match the server count (" + characterCount + " != " + characters.Count + "mission: " + Prefab.Identifier + ")"); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 0e9692371..40ee18f21 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -208,6 +208,19 @@ namespace Barotrauma get { return pauseMenuOpen; } } + public static bool InputBlockingMenuOpen + { + get + { + return PauseMenuOpen || + SettingsMenuOpen || + DebugConsole.IsOpen || + GameSession.IsTabMenuOpen || + (GameMain.GameSession?.GameMode?.Paused ?? false) || + CharacterHUD.IsCampaignInterfaceOpen; + } + } + public static bool PreventPauseMenuToggle = false; public static Color ScreenOverlayColor @@ -226,6 +239,9 @@ namespace Barotrauma private static SavingIndicatorState savingIndicatorState = SavingIndicatorState.None; private static float? timeUntilSavingIndicatorDisabled; + private static string loadedSpritesText; + private static DateTime loadedSpritesUpdateTime; + private enum SavingIndicatorState { None, @@ -441,9 +457,12 @@ namespace Barotrauma "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, Color.Lerp(GUI.Style.Green, GUI.Style.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(10, 115), - "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)", - Color.White, Color.Black * 0.5f, 0, SmallFont); + if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime) + { + loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)"; + loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5); + } + DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, SmallFont); if (debugDrawSounds) { @@ -978,8 +997,7 @@ namespace Barotrauma return editor.GetMouseCursorState(); // Portrait area during gameplay case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true): - if (HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) || - Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarArea).Contains(PlayerInput.MousePosition)) + if (HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) || CharacterHealth.IsMouseOnHealthBar()) { return CursorState.Hand; } @@ -1331,7 +1349,7 @@ namespace Barotrauma /// Should the indicator move based on the camera position? /// Override the distance-based alpha value with the specified alpha value - public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, + public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Vector2 visibleRange, Sprite sprite, in Color color, bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { Vector2 diff = worldPosition - cam.WorldViewCenter; @@ -1339,9 +1357,9 @@ namespace Barotrauma float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; - if (overrideAlpha.HasValue || dist > hideDist) + if (overrideAlpha.HasValue || (dist > visibleRange.X && dist < visibleRange.Y)) { - float alpha = overrideAlpha ?? Math.Min((dist - hideDist) / 100.0f, 1.0f); + float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.X) / 100.0f, 1.0f - ((dist - visibleRange.Y + 100f) / 100.0f), 1.0f); Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); if (!createOffset) @@ -1352,8 +1370,9 @@ namespace Barotrauma float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos); float angle = MathUtils.VectorToAngle(diff); + float originalAngle = angle; - float minAngleDiff = 0.05f; + const float minAngleDiff = 0.05f; bool overlapFound = true; int iterations = 0; while (overlapFound && iterations < 10) @@ -1375,18 +1394,24 @@ namespace Barotrauma usedIndicatorAngles.Add(angle); - Vector2 unclampedDiff = new Vector2( - (float)Math.Cos(angle) * screenDist, - (float)-Math.Sin(angle) * screenDist); - Vector2 iconDiff = new Vector2( + (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist + 10), + (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist + 10)); + + angle = MathHelper.Lerp(originalAngle, angle, MathHelper.Clamp(((screenDist + 10f) - iconDiff.Length()) / 10f, 0f, 1f)); + + /*Vector2 unclampedDiff = new Vector2( + (float)Math.Cos(angle) * screenDist, + (float)-Math.Sin(angle) * screenDist);*/ + + iconDiff = new Vector2( (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist), (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist)); Vector2 iconPos = cam.WorldToScreen(cam.WorldViewCenter) + iconDiff; sprite.Draw(spriteBatch, iconPos, color * alpha, rotate: 0.0f, scale: symbolScale); - if (unclampedDiff.Length() - 10 > iconDiff.Length()) + if (/*unclampedDiff.Length()*/ screenDist - 10 > iconDiff.Length()) { Vector2 normalizedDiff = Vector2.Normalize(targetScreenPos - iconPos); Vector2 arrowOffset = normalizedDiff * sprite.size.X * symbolScale * 0.7f; @@ -1395,6 +1420,12 @@ namespace Barotrauma } } + public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, + bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) + { + DrawIndicator(spriteBatch, worldPosition, cam, new Vector2(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha); + } + public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) { DrawLine(sb, t, start, end, clr, depth, (int)width); @@ -1495,6 +1526,22 @@ namespace Barotrauma } } + public static void DrawFilledRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, float depth = 0.0f) + { + if (size.X < 0) + { + start.X += size.X; + size.X = -size.X; + } + if (size.Y < 0) + { + start.Y += size.Y; + size.Y = -size.Y; + } + + sb.Draw(t, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth); + } + public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1) { Matrix rotate = Matrix.CreateRotationZ(rotation); @@ -1971,6 +2018,30 @@ namespace Barotrauma } return frame; } + + public static GUIMessageBox AskForConfirmation(string header, string body, Action onConfirm, Action onDeny = null) + { + string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + + // Cancel button + msgBox.Buttons[1].OnClicked = delegate + { + onDeny?.Invoke(); + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[0].OnClicked = delegate + { + onConfirm.Invoke(); + msgBox.Close(); + return true; + }; + return msgBox; + } + #endregion #region Element positioning @@ -2098,7 +2169,7 @@ namespace Barotrauma for (int j = i + 1; j < elements.Count; j++) { Rectangle rect2 = elements[j].Rect; - if (!rect1.Intersects(rect2)) continue; + if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; @@ -2127,10 +2198,10 @@ namespace Barotrauma elements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint(); } - if (disallowedAreas == null) continue; + if (disallowedAreas == null) { continue; } foreach (Rectangle rect2 in disallowedAreas) { - if (!rect1.Intersects(rect2)) continue; + if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; @@ -2148,7 +2219,7 @@ namespace Barotrauma iterations++; } - Vector2 ClampMoveAmount(Rectangle Rect, Rectangle clampTo, Vector2 moveAmount) + static Vector2 ClampMoveAmount(Rectangle Rect, Rectangle clampTo, Vector2 moveAmount) { if (Rect.Y < clampTo.Y) { @@ -2221,6 +2292,7 @@ namespace Barotrauma } }; + bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) { if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode) @@ -2253,45 +2325,22 @@ namespace Barotrauma }; return true; }; - var saveAndQuitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) + if (IsOutpostLevel()) { - UserData = "save" - }; - saveAndQuitButton.OnClicked += (btn, userdata) => - { - //Only allow saving mid-round in outpost levels. Quitting in the middle of a mission reset progress to the start of the round. - if (GameMain.GameSession == null) + var saveAndQuitButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), TextManager.Get("PauseMenuSaveQuit")) { - pauseMenuOpen = false; - - } - else if (GameMain.GameSession?.Campaign == null || Level.IsLoadedOutpost) - { - pauseMenuOpen = false; - GameMain.QuitToMainMenu(save: true); - } - else - { - var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuSaveAndQuitVerification", fallBackTag: "pausemenuquitverification"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (_, userdata) => + UserData = "save", + OnClicked = (btn, userData) => { pauseMenuOpen = false; - GameMain.QuitToMainMenu(save: false); + if (IsOutpostLevel()) + { + GameMain.QuitToMainMenu(save: true); + } return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = (_, userdata) => - { - pauseMenuOpen = false; - msgBox.Close(); - return true; - }; - } - return true; - }; + } + }; + } } else if (GameMain.GameSession.GameMode is TestGameMode) { @@ -2313,7 +2362,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (!GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { return false; } - if (GameMain.GameSession.GameMode is CampaignMode || (!Submarine.MainSub.AtStartExit && !Submarine.MainSub.AtEndExit)) + if (GameMain.GameSession.GameMode is CampaignMode && !IsOutpostLevel() || (!Submarine.MainSub.AtStartExit && !Submarine.MainSub.AtEndExit)) { var msgBox = new GUIMessageBox("", TextManager.Get(GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd"), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 337e98bc8..94a503f1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -201,7 +201,7 @@ namespace Barotrauma { base.ApplyStyle(style); - if (frame != null) { frame.ApplyStyle(style); } + frame?.ApplyStyle(style); } public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectInflate = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index a6867eb60..6b7265865 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -59,9 +59,7 @@ namespace Barotrauma private bool useGridLayout; - private float targetScroll; - - private GUIComponent pendingScroll; + private GUIComponent scrollToElement; public bool AllowMouseWheelScroll { get; set; } = true; @@ -238,8 +236,6 @@ namespace Barotrauma public GUIComponent DraggedElement => draggedElement; - private bool scheduledScroll = false; - private readonly bool isHorizontal; /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. @@ -429,7 +425,14 @@ namespace Barotrauma int index = children.IndexOf(component); if (index < 0) { return; } - targetScroll = MathHelper.Clamp(MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, (children.Count - 0.9f), index)), ScrollBar.MinValue, ScrollBar.MaxValue); + if (!Content.Children.Contains(component) || !component.Visible) + { + scrollToElement = null; + } + else + { + scrollToElement = component; + } } public void ScrollToEnd(float duration) @@ -533,7 +536,7 @@ namespace Barotrauma } } - if (SelectTop && Content.Children.Any() && pendingScroll == null) + if (SelectTop && Content.Children.Any() && scrollToElement == null) { GUIComponent component = Content.Children.FirstOrDefault(c => (c.Rect.Y - Content.Rect.Y) / (float)c.Rect.Height > -0.1f); @@ -563,7 +566,6 @@ namespace Barotrauma { if (SelectTop) { - pendingScroll = child; ScrollToElement(child); Select(i, autoScroll: false, takeKeyBoardFocus: true); } @@ -728,25 +730,29 @@ namespace Barotrauma } } } - - if (SmoothScroll) - { - if (targetScroll > -1) - { - float distance = Math.Abs(targetScroll - BarScroll); - float speed = Math.Max(distance * BarSize, 0.1f); - BarScroll = (1.0f - speed) * BarScroll + speed * targetScroll; - if (MathUtils.NearlyEqual(BarScroll, targetScroll) || GUIScrollBar.DraggingBar != null) - { - targetScroll = -1; - pendingScroll = null; - } - } - } + if (scrollToElement != null) + { + if (!scrollToElement.Visible || !Content.Children.Contains(scrollToElement)) + { + scrollToElement = null; + } + else + { + float diff = isHorizontal ? scrollToElement.Rect.X - Content.Rect.X : scrollToElement.Rect.Y - Content.Rect.Y; + float speed = MathHelper.Clamp(Math.Abs(diff) * 0.1f, 5.0f, 100.0f); + System.Diagnostics.Debug.WriteLine(speed); + if (Math.Abs(diff) < speed || GUIScrollBar.DraggingBar != null) + { + speed = Math.Abs(diff); + scrollToElement = null; + } + BarScroll += speed * Math.Sign(diff) / TotalSize; + } + } + if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && AllowMouseWheelScroll && PlayerInput.ScrollWheelSpeed != 0) { - float speed = PlayerInput.ScrollWheelSpeed / 500.0f * BarSize; if (SmoothScroll) { if (ClampScrollToElements) @@ -762,13 +768,6 @@ namespace Barotrauma SelectNext(takeKeyBoardFocus: true); } } - else - { - pendingScroll = null; - if (targetScroll < 0) { targetScroll = BarScroll; } - targetScroll -= speed; - targetScroll = Math.Clamp(targetScroll, ScrollBar.MinValue, ScrollBar.MaxValue); - } } else { @@ -799,7 +798,6 @@ namespace Barotrauma Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); if (SmoothScroll) { - pendingScroll = child; ScrollToElement(child); } break; @@ -819,7 +817,6 @@ namespace Barotrauma Select(index, force, !SmoothScroll && autoScroll, takeKeyBoardFocus: takeKeyBoardFocus); if (SmoothScroll) { - pendingScroll = child; ScrollToElement(child); } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 0d7d27fd5..232c13c6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -551,7 +551,7 @@ namespace Barotrauma } if (openState >= 2.0f) { - if (Parent != null) { Parent.RemoveChild(this); } + Parent?.RemoveChild(this); if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } } } @@ -604,7 +604,7 @@ namespace Barotrauma } else { - if (Parent != null) { Parent.RemoveChild(this); } + Parent?.RemoveChild(this); if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index dae7f109d..e512e979a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -140,6 +140,7 @@ namespace Barotrauma { if (value == intValue) { return; } intValue = value; + ClampIntValue(); UpdateText(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 952cded4d..5d89dcaa0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -658,10 +658,7 @@ namespace Barotrauma currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData); } - if (Strikethrough != null) - { - Strikethrough.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); - } + Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); } if (overflowClipActive) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 865184d16..4d8a2feae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -49,8 +49,11 @@ namespace Barotrauma get { return _caretIndex; } set { - _caretIndex = value; - caretPosDirty = true; + if (value >= 0) + { + _caretIndex = value; + caretPosDirty = true; + } } } private bool caretPosDirty; @@ -454,7 +457,7 @@ namespace Barotrauma } if (!isSelecting) { - isSelecting = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); + isSelecting = PlayerInput.IsShiftDown(); } if (mouseHeldInside && !PlayerInput.PrimaryMouseButtonHeld()) @@ -879,15 +882,22 @@ namespace Barotrauma selectionEndIndex = Math.Min(CaretIndex, textDrawn.Length); selectionEndPos = caretPos; selectedCharacters = Math.Abs(selectionStartIndex - selectionEndIndex); - if (IsLeftToRight) + try { - selectedText = Text.Substring(selectionStartIndex, selectedCharacters); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, selectedCharacters)) * TextBlock.TextScale; + if (IsLeftToRight) + { + selectedText = Text.Substring(selectionStartIndex, Math.Min(selectedCharacters, Text.Length)); + selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; + } + else + { + selectedText = Text.Substring(selectionEndIndex, Math.Min(selectedCharacters, Text.Length)); + selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; + } } - else + catch (ArgumentOutOfRangeException exception) { - selectedText = Text.Substring(selectionEndIndex, Math.Min(selectedCharacters, textDrawn.Length - selectionEndIndex)); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, selectedCharacters)) * TextBlock.TextScale; + DebugConsole.ThrowError($"GUITextBox: Invalid selection: ({exception})"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 3272d4940..3771c3a7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -17,47 +17,63 @@ namespace Barotrauma private readonly Dictionary tabLists = new Dictionary(); private readonly Dictionary tabSortingMethods = new Dictionary(); private readonly List itemsToSell = new List(); + private readonly List itemsToSellFromSub = new List(); private StoreTab activeTab = StoreTab.Buy; private MapEntityCategory? selectedItemCategory; private bool suppressBuySell; - private int buyTotal, sellTotal; + private int buyTotal, sellTotal, sellFromSubTotal; private GUITextBlock merchantBalanceBlock; private GUITextBlock currentSellValueBlock, newSellValueBlock; private GUIImage sellValueChangeArrow; private GUIDropDown sortingDropDown; private GUITextBox searchBox; - private GUIListBox storeBuyList, storeSellList; + private GUIListBox storeBuyList, storeSellList, storeSellFromSubList; /// /// Can be null when there are no deals at the current location /// - private GUILayoutGroup storeDailySpecialsGroup, storeRequestedGoodGroup; + private GUILayoutGroup storeDailySpecialsGroup, storeRequestedGoodGroup, storeRequestedSubGoodGroup; private Color storeSpecialColor; - private GUIListBox shoppingCrateBuyList, shoppingCrateSellList; + private GUIListBox shoppingCrateBuyList, shoppingCrateSellList, shoppingCrateSellFromSubList; private GUITextBlock shoppingCrateTotal; private GUIButton clearAllButton, confirmButton; - private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh; + private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh; private Point resolutionWhenCreated; private bool hadPermissions; - private Dictionary OwnedItems { get; } = new Dictionary(); + private Dictionary OwnedItems { get; } = new Dictionary(); private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; private int PlayerMoney => campaignUI.Campaign.Money; private bool HasPermissions => campaignUI.Campaign.AllowedToManageCampaign(); - private bool IsBuying => activeTab != StoreTab.Sell; - private bool IsSelling => activeTab == StoreTab.Sell; - private GUIListBox ActiveShoppingCrateList => IsBuying ? shoppingCrateBuyList : shoppingCrateSellList; + private bool IsBuying => activeTab switch + { + StoreTab.Buy => true, + StoreTab.Sell => false, + StoreTab.SellFromSub => false, + _ => throw new NotImplementedException() + }; + private bool IsSelling => !IsBuying; + private GUIListBox ActiveShoppingCrateList => activeTab switch + { + StoreTab.Buy => shoppingCrateBuyList, + StoreTab.Sell => shoppingCrateSellList, + StoreTab.SellFromSub => shoppingCrateSellFromSubList, + _ => throw new NotImplementedException() + }; - private enum StoreTab + private bool IsTabUnavailable(StoreTab tab) => !tabLists.ContainsKey(tab); + + public enum StoreTab { Buy, - Sell + Sell, + SellFromSub } private enum SortingMethod @@ -73,11 +89,8 @@ namespace Barotrauma { this.campaignUI = campaignUI; this.parentComponent = parentComponent; - hadPermissions = HasPermissions; - CreateUI(); - campaignUI.Campaign.Map.OnLocationChanged += UpdateLocation; if (CurrentLocation?.Reputation != null) { @@ -89,8 +102,10 @@ namespace Barotrauma campaignUI.Campaign.CargoManager.OnSoldItemsChanged += () => { needsItemsToSellRefresh = true; + needsItemsToSellFromSubRefresh = true; needsRefresh = true; }; + campaignUI.Campaign.CargoManager.OnItemsInSellFromSubCrateChanged += () => { needsSellingFromSubRefresh = true; }; } public void Refresh(bool updateOwned = true) @@ -99,6 +114,7 @@ namespace Barotrauma if (updateOwned) { UpdateOwnedItems(); } RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); + RefreshSellingFromSub(updateOwned: false); needsRefresh = false; } @@ -124,6 +140,20 @@ namespace Barotrauma needsSellingRefresh = false; } + private void RefreshSellingFromSub(bool updateOwned = true, bool updateItemsToSellFromSub = true) + { + if (IsTabUnavailable(StoreTab.SellFromSub)) { return; } + if (updateOwned) { UpdateOwnedItems(); } + if (updateItemsToSellFromSub) RefreshItemsToSellFromSub(); + RefreshShoppingCrateSellFromSubList(); + RefreshStoreSellFromSubList(); + // TODO: Separate permissions from regular campaign permissions + var hasPermissions = HasPermissions; + storeSellFromSubList.Enabled = hasPermissions; + shoppingCrateSellFromSubList.Enabled = hasPermissions; + needsSellingFromSubRefresh = false; + } + private void CreateUI() { if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild) @@ -236,9 +266,13 @@ namespace Barotrauma { if (CurrentLocation != null) { - int balanceAfterTransaction = IsBuying ? - CurrentLocation.StoreCurrentBalance + buyTotal : - CurrentLocation.StoreCurrentBalance - sellTotal; + int balanceAfterTransaction = activeTab switch + { + StoreTab.Buy => CurrentLocation.StoreCurrentBalance + buyTotal, + StoreTab.Sell => CurrentLocation.StoreCurrentBalance - sellTotal, + StoreTab.SellFromSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, + _ => throw new NotImplementedException(), + }; if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) { var newStatus = Location.GetStoreBalanceStatus(balanceAfterTransaction); @@ -300,8 +334,14 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { + if (tab == StoreTab.SellFromSub && GameMain.IsMultiplayer) { continue; } + string text = tab switch + { + StoreTab.SellFromSub => TextManager.Get("submarine"), + _ => TextManager.Get("campaignstoretab." + tab) + }; var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform), - text: TextManager.Get("campaignstoretab." + tab), style: "GUITabButton") + text: text, style: "GUITabButton") { UserData = tab, OnClicked = (button, userData) => @@ -416,6 +456,17 @@ namespace Barotrauma storeRequestedGoodGroup = CreateDealsGroup(storeSellList); tabLists.Add(StoreTab.Sell, storeSellList); + if (GameMain.IsSingleplayer) + { + storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) + { + AutoHideScrollBar = false, + Visible = false + }; + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); + tabLists.Add(StoreTab.SellFromSub, storeSellFromSubList); + } + // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.TopRight) @@ -475,6 +526,10 @@ namespace Barotrauma var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), shoppingCrateInventoryContainer.RectTransform), style: null); shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; + if (GameMain.IsSingleplayer) + { + shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; + } var totalContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) { @@ -504,7 +559,13 @@ namespace Barotrauma OnClicked = (button, userData) => { if (!HasPermissions) { return false; } - var itemsToRemove = new List(IsBuying ? CargoManager.ItemsInBuyCrate : CargoManager.ItemsInSellCrate); + var itemsToRemove = activeTab switch + { + StoreTab.Buy => new List(CargoManager.ItemsInBuyCrate), + StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), + StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), + _ => throw new NotImplementedException(), + }; itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); return true; } @@ -560,6 +621,7 @@ namespace Barotrauma private void ChangeStoreTab(StoreTab tab) { + if (IsTabUnavailable(tab)) { return; } activeTab = tab; foreach (GUIButton tabButton in storeTabButtons) { @@ -571,24 +633,56 @@ namespace Barotrauma SetConfirmButtonBehavior(); SetConfirmButtonStatus(); FilterStoreItems(); - if (tab == StoreTab.Buy) + switch (tab) { - storeSellList.Visible = false; - storeBuyList.Visible = true; - shoppingCrateSellList.Visible = false; - shoppingCrateBuyList.Visible = true; - } - else if (tab == StoreTab.Sell) - { - storeBuyList.Visible = false; - storeSellList.Visible = true; - shoppingCrateBuyList.Visible = false; - shoppingCrateSellList.Visible = true; + case StoreTab.Buy: + storeSellList.Visible = false; + if (storeSellFromSubList != null) + { + storeSellFromSubList.Visible = false; + } + storeBuyList.Visible = true; + shoppingCrateSellList.Visible = false; + if (shoppingCrateSellFromSubList != null) + { + shoppingCrateSellFromSubList.Visible = false; + } + shoppingCrateBuyList.Visible = true; + break; + case StoreTab.Sell: + storeBuyList.Visible = false; + if (storeSellFromSubList != null) + { + storeSellFromSubList.Visible = false; + } + storeSellList.Visible = true; + shoppingCrateBuyList.Visible = false; + if (shoppingCrateSellFromSubList != null) + { + shoppingCrateSellFromSubList.Visible = false; + } + shoppingCrateSellList.Visible = true; + break; + case StoreTab.SellFromSub: + storeBuyList.Visible = false; + storeSellList.Visible = false; + if (storeSellFromSubList != null) + { + storeSellFromSubList.Visible = true; + } + shoppingCrateBuyList.Visible = false; + shoppingCrateSellList.Visible = false; + if (shoppingCrateSellFromSubList != null) + { + shoppingCrateSellFromSubList.Visible = true; + } + break; } } private void FilterStoreItems(MapEntityCategory? category, string filter) { + if (IsTabUnavailable(activeTab)) { return; } selectedItemCategory = category; var list = tabLists[activeTab]; filter = filter?.ToLower(); @@ -668,7 +762,7 @@ namespace Barotrauma if (itemFrame == null) { var parentComponent = isDailySpecial ? storeDailySpecialsGroup : storeBuyList as GUIComponent; - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, quantity), priceInfo, parentComponent, forceDisable: !hasPermissions); + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, quantity), parentComponent, StoreTab.Buy, forceDisable: !hasPermissions); } else { @@ -688,7 +782,7 @@ namespace Barotrauma removedItemFrames.AddRange(storeDailySpecialsGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (IsBuying) { FilterStoreItems(); } + if (activeTab == StoreTab.Buy) { FilterStoreItems(); } SortItems(StoreTab.Buy); storeBuyList.BarScroll = prevBuyListScroll; @@ -743,7 +837,7 @@ namespace Barotrauma if (itemFrame == null) { var parentComponent = isRequestedGood ? storeRequestedGoodGroup : storeSellList as GUIComponent; - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), priceInfo, parentComponent, forceDisable: !hasPermissions); + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.Sell, forceDisable: !hasPermissions); } else { @@ -766,13 +860,91 @@ namespace Barotrauma removedItemFrames.AddRange(storeRequestedGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (IsSelling) { FilterStoreItems(); } + if (activeTab == StoreTab.Sell) { FilterStoreItems(); } SortItems(StoreTab.Sell); storeSellList.BarScroll = prevSellListScroll; shoppingCrateSellList.BarScroll = prevShoppingCrateScroll; } + private void RefreshStoreSellFromSubList() + { + float prevSellListScroll = storeSellFromSubList.BarScroll; + float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; + bool hasPermissions = HasPermissions; + HashSet existingItemFrames = new HashSet(); + + if ((storeRequestedSubGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) + { + if (storeRequestedSubGoodGroup == null) + { + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellList); + storeRequestedSubGoodGroup.Parent.SetAsFirstChild(); + } + else + { + storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup.Parent); + storeRequestedSubGoodGroup = null; + } + storeSellFromSubList.RecalculateChildren(); + } + + foreach (PurchasedItem item in itemsToSellFromSub) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + + foreach (var requestedGood in CurrentLocation.RequestedGoods) + { + if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } + + void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) + { + PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + if (priceInfo == null) { return; } + var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + var itemFrame = isRequestedGood ? + storeRequestedSubGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : + storeSellFromSubList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); + if (CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellFromSubCrate) + { + itemQuantity = Math.Max(itemQuantity - itemInSellFromSubCrate.Quantity, 0); + } + if (itemFrame == null) + { + var parentComponent = isRequestedGood ? storeRequestedSubGoodGroup : storeSellFromSubList as GUIComponent; + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellFromSub, forceDisable: !hasPermissions); + } + else + { + (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; + SetQuantityLabelText(StoreTab.SellFromSub, itemFrame); + SetOwnedLabelText(itemFrame); + SetPriceGetters(itemFrame, false); + } + SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); + if (itemQuantity < 1 && !isRequestedGood) + { + itemFrame.Visible = false; + } + existingItemFrames.Add(itemFrame); + } + + var removedItemFrames = storeSellFromSubList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList(); + if (storeRequestedSubGoodGroup != null) + { + removedItemFrames.AddRange(storeRequestedSubGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); + } + removedItemFrames.ForEach(f => f.RectTransform.Parent = null); + if (activeTab == StoreTab.SellFromSub) { FilterStoreItems(); } + SortItems(StoreTab.SellFromSub); + + storeSellFromSubList.BarScroll = prevSellListScroll; + shoppingCrateSellFromSubList.BarScroll = prevShoppingCrateScroll; + } + private void SetPriceGetters(GUIComponent itemFrame, bool buying) { if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } @@ -834,7 +1006,38 @@ namespace Barotrauma needsItemsToSellRefresh = false; } - private void RefreshShoppingCrateList(List items, GUIListBox listBox) + public void RefreshItemsToSellFromSub() + { + itemsToSellFromSub.Clear(); + var subItems = CargoManager.GetSellableItemsFromSub(); + foreach (Item subItem in subItems) + { + if (itemsToSellFromSub.FirstOrDefault(i => i.ItemPrefab == subItem.Prefab) is PurchasedItem item) + { + item.Quantity += 1; + } + else if (subItem.Prefab.GetPriceInfo(CurrentLocation) != null) + { + itemsToSellFromSub.Add(new PurchasedItem(subItem.Prefab, 1)); + } + } + + // Remove items from sell crate if they aren't on the sub anymore + var itemsInCrate = new List(CargoManager.ItemsInSellFromSubCrate); + foreach (PurchasedItem crateItem in itemsInCrate) + { + var subItem = itemsToSellFromSub.Find(i => i.ItemPrefab == crateItem.ItemPrefab); + var subItemQuantity = subItem != null ? subItem.Quantity : 0; + if (crateItem.Quantity > subItemQuantity) + { + CargoManager.ModifyItemQuantityInSellFromSubCrate(crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity); + } + } + sellableItemsFromSubUpdateTimer = 0.0f; + needsItemsToSellFromSubRefresh = false; + } + + private void RefreshShoppingCrateList(List items, GUIListBox listBox, StoreTab tab) { bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); @@ -848,7 +1051,7 @@ namespace Barotrauma GUINumberInput numInput = null; if (itemFrame == null) { - itemFrame = CreateItemFrame(item, priceInfo, listBox, forceDisable: !hasPermissions); + itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions); numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput; } else @@ -859,6 +1062,7 @@ namespace Barotrauma { numInput.UserData = item; numInput.Enabled = hasPermissions; + numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); } SetOwnedLabelText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); @@ -873,7 +1077,7 @@ namespace Barotrauma } suppressBuySell = false; - var price = listBox == shoppingCrateBuyList ? + var price = tab == StoreTab.Buy ? CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo) : CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); totalPrice += item.Quantity * price; @@ -883,24 +1087,32 @@ namespace Barotrauma removedItemFrames.ForEach(f => listBox.Content.RemoveChild(f)); SortItems(listBox, SortingMethod.CategoryAsc); - listBox.UpdateScrollBarSize(); - if (listBox == shoppingCrateBuyList) + listBox.UpdateScrollBarSize(); + switch (tab) { - buyTotal = totalPrice; - if (IsBuying) { SetShoppingCrateTotalText(); } + case StoreTab.Buy: + buyTotal = totalPrice; + break; + case StoreTab.Sell: + sellTotal = totalPrice; + break; + case StoreTab.SellFromSub: + sellFromSubTotal = totalPrice; + break; } - else + if (activeTab == tab) { - sellTotal = totalPrice; - if(IsSelling) { SetShoppingCrateTotalText(); } + SetShoppingCrateTotalText(); } SetClearAllButtonStatus(); SetConfirmButtonStatus(); } - private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.ItemsInBuyCrate, shoppingCrateBuyList); + private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.ItemsInBuyCrate, shoppingCrateBuyList, StoreTab.Buy); - private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList); + private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList, StoreTab.Sell); + + private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellFromSub); private void SortItems(GUIListBox list, SortingMethod sortingMethod) { @@ -932,7 +1144,7 @@ namespace Barotrauma else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortItems(list, SortingMethod.AlphabeticalAsc); - if (list == storeSellList || list == shoppingCrateSellList) + if (list != storeBuyList && list != shoppingCrateBuyList) { list.Content.RectTransform.SortChildren(CompareBySellPrice); if (GetSpecialsGroup() is GUILayoutGroup specialsGroup) @@ -1014,6 +1226,10 @@ namespace Barotrauma { return storeRequestedGoodGroup; } + else if (list == storeSellFromSubList) + { + return storeRequestedSubGoodGroup; + } else { return null; @@ -1045,15 +1261,20 @@ namespace Barotrauma private void SortItems(StoreTab tab, SortingMethod sortingMethod) { + if (IsTabUnavailable(tab)) { return; } tabSortingMethods[tab] = sortingMethod; SortItems(tabLists[tab], sortingMethod); } - private void SortItems(StoreTab tab) => SortItems(tab, tabSortingMethods[tab]); + private void SortItems(StoreTab tab) + { + if (IsTabUnavailable(tab)) { return; } + SortItems(tab, tabSortingMethods[tab]); + } private void SortActiveTabItems(SortingMethod sortingMethod) => SortItems(activeTab, sortingMethod); - private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIComponent parentComponent, bool forceDisable = false) + private GUIComponent CreateItemFrame(PurchasedItem pi, GUIComponent parentComponent, StoreTab containingTab, bool forceDisable = false) { var tooltip = pi.ItemPrefab.Name; if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) @@ -1114,8 +1335,8 @@ namespace Barotrauma CanBeFocused = false, Stretch = true }; - var isSellingRelatedList = parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup || parentComponent == shoppingCrateSellList; - var locationHasDealOnItem = isSellingRelatedList ? + bool isSellingRelatedList = containingTab != StoreTab.Buy; + bool locationHasDealOnItem = isSellingRelatedList ? CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) @@ -1140,14 +1361,15 @@ namespace Barotrauma }; dealIcon.SetAsFirstChild(); } - var isParentOnLeftSideOfInterface = parentComponent == storeBuyList || parentComponent == storeDailySpecialsGroup || - parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup; + bool isParentOnLeftSideOfInterface = parentComponent == storeBuyList || parentComponent == storeDailySpecialsGroup || + parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup || + parentComponent == storeSellFromSubList || parentComponent == storeRequestedSubGoodGroup; GUILayoutGroup shoppingCrateAmountGroup = null; GUINumberInput amountInput = null; if (isParentOnLeftSideOfInterface) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform), - CreateQuantityLabelText(isSellingRelatedList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) + CreateQuantityLabelText(containingTab, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1156,7 +1378,7 @@ namespace Barotrauma UserData = "quantitylabel" }; } - else if (!isParentOnLeftSideOfInterface) + else { var relativePadding = nameBlock.Padding.X / nameBlock.Rect.Width; shoppingCrateAmountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativePadding, 0.6f), nameAndQuantityGroup.RectTransform) { RelativeOffset = new Vector2(relativePadding, 0) }, @@ -1167,7 +1389,7 @@ namespace Barotrauma amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), GUINumberInput.NumberType.Int) { MinValueInt = 0, - MaxValueInt = GetMaxAvailable(pi.ItemPrefab, isSellingRelatedList ? StoreTab.Sell : StoreTab.Buy), + MaxValueInt = GetMaxAvailable(pi.ItemPrefab, containingTab), UserData = pi, IntValue = pi.Quantity }; @@ -1394,7 +1616,7 @@ namespace Barotrauma } } - private string CreateQuantityLabelText(StoreTab mode, int quantity) => mode == StoreTab.Sell ? + private string CreateQuantityLabelText(StoreTab mode, int quantity) => mode != StoreTab.Buy ? TextManager.GetWithVariable("campaignstore.quantity", "[amount]", quantity.ToString()) : TextManager.GetWithVariable("campaignstore.instock", "[amount]", quantity.ToString()); @@ -1417,10 +1639,16 @@ namespace Barotrauma private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { - var list = mode == StoreTab.Sell ? itemsToSell : CurrentLocation.StoreStock; + var list = mode switch + { + StoreTab.Buy => CurrentLocation.StoreStock, + StoreTab.Sell => itemsToSell, + StoreTab.SellFromSub => itemsToSellFromSub, + _ => throw new NotImplementedException() + }; if (list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item) { - if (mode != StoreTab.Sell) + if (mode == StoreTab.Buy) { var purchasedItem = CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab); if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } @@ -1469,11 +1697,37 @@ namespace Barotrauma return false; } - private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) => IsBuying ? - ModifyBuyQuantity(item, quantity) : ModifySellQuantity(item, quantity); + private bool ModifySellFromSubQuantity(PurchasedItem item, int quantity) + { + if (item == null || item.ItemPrefab == null) { return false; } + if (!HasPermissions) { return false; } + if (quantity > 0) + { + // Make sure there's enough available to sell + var itemToSell = CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; + if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellFromSub)) { return false; } + } + CargoManager.ModifyItemQuantityInSellFromSubCrate(item.ItemPrefab, quantity); + // TODO: GameMain.Client?.SendCampaignState(); + return false; + } - private bool ClearFromShoppingCrate(PurchasedItem item) => IsBuying ? - ModifyBuyQuantity(item, -item.Quantity) : ModifySellQuantity(item, -item.Quantity); + private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1) => activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, quantity), + StoreTab.Sell => ModifySellQuantity(item, quantity), + StoreTab.SellFromSub => ModifySellFromSubQuantity(item, quantity), + _ => throw new NotImplementedException(), + }; + + private bool ClearFromShoppingCrate(PurchasedItem item) => activeTab switch + { + StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity), + StoreTab.Sell => ModifySellQuantity(item, -item.Quantity), + StoreTab.SellFromSub => ModifySellFromSubQuantity(item, -item.Quantity), + _ => throw new NotImplementedException(), + }; private bool BuyItems() { @@ -1510,18 +1764,17 @@ namespace Barotrauma private bool SellItems() { if (!HasPermissions) { return false; } - - var itemsToSell = new List(CargoManager.ItemsInSellCrate); + var itemsToSell = activeTab switch + { + StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), + StoreTab.SellFromSub => new List(CargoManager.ItemsInSellFromSubCrate), + _ => throw new NotImplementedException() + }; var itemsToRemove = new List(); var totalValue = 0; foreach (PurchasedItem item in itemsToSell) { - if (item?.ItemPrefab == null) - { - itemsToRemove.Add(item); - continue; - } - if (item.ItemPrefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) + if (item?.ItemPrefab?.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) { totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); } @@ -1531,12 +1784,13 @@ namespace Barotrauma } } itemsToRemove.ForEach(i => itemsToSell.Remove(i)); - if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } - - CargoManager.SellItems(itemsToSell); - GameMain.Client?.SendCampaignState(); - + CargoManager.SellItems(itemsToSell, activeTab); + if (activeTab == StoreTab.Sell) + { + // TODO: Implement selling sub items in multiplayer + GameMain.Client?.SendCampaignState(); + } return false; } @@ -1549,8 +1803,14 @@ namespace Barotrauma } else { - shoppingCrateTotal.Text = GetCurrencyFormatted(sellTotal); - shoppingCrateTotal.TextColor = CurrentLocation != null && sellTotal > CurrentLocation.StoreCurrentBalance ? Color.Red : Color.White; + int total = activeTab switch + { + StoreTab.Sell => sellTotal, + StoreTab.SellFromSub => sellFromSubTotal, + _ => throw new NotImplementedException(), + }; + shoppingCrateTotal.Text = GetCurrencyFormatted(total); + shoppingCrateTotal.TextColor = CurrentLocation != null && total > CurrentLocation.StoreCurrentBalance ? Color.Red : Color.White; } } @@ -1580,13 +1840,19 @@ namespace Barotrauma private void SetConfirmButtonStatus() => confirmButton.Enabled = HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any() && - ((IsBuying && buyTotal <= PlayerMoney) || (IsSelling && CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance)); + activeTab switch + { + StoreTab.Buy => buyTotal <= PlayerMoney, + StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, + StoreTab.SellFromSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, + _ => throw new NotImplementedException(), + }; private void SetClearAllButtonStatus() => clearAllButton.Enabled = HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any(); - private float ownedItemsUpdateTimer = 0.0f; - private readonly float ownedItemsUpdateInterval = 1.5f; + private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f; + private readonly float timerUpdateInterval = 1.5f; public void Update(float deltaTime) { @@ -1598,7 +1864,7 @@ namespace Barotrauma { // Update the owned items at short intervals and check if the interface should be refreshed ownedItemsUpdateTimer += deltaTime; - if (ownedItemsUpdateTimer >= ownedItemsUpdateInterval) + if (ownedItemsUpdateTimer >= timerUpdateInterval) { var prevOwnedItems = new Dictionary(OwnedItems); UpdateOwnedItems(); @@ -1612,12 +1878,21 @@ namespace Barotrauma needsRefresh = true; } } + // Update the sellable sub items at short intervals and check if the interface should be refreshed + sellableItemsFromSubUpdateTimer += deltaTime; + if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval) + { + needsItemsToSellFromSubRefresh = true; + needsRefresh = true; + } } if (needsItemsToSellRefresh) { RefreshItemsToSell(); } + if (needsItemsToSellFromSubRefresh) { RefreshItemsToSellFromSub(); } if (needsRefresh || hadPermissions != HasPermissions) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } if (needsBuyingRefresh) { RefreshBuying(); } if (needsSellingRefresh) { RefreshSelling(); } + if (needsSellingFromSubRefresh) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 838f4c534..409bb15fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -12,7 +12,8 @@ namespace Barotrauma private const int submarinesPerPage = 4; private int currentPage = 1; private int pageCount; - private bool transferService, purchaseService, initialized; + private readonly bool transferService, purchaseService; + private bool initialized; private int deliveryFee; private string deliveryLocationName; @@ -27,12 +28,12 @@ namespace Barotrauma private int selectionIndicatorThickness; private GUIImage listBackground; - private List subsToShow; - private SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; + private readonly List subsToShow; + private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; private string purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; - private RectTransform parent; - private Action closeAction; + private readonly RectTransform parent; + private readonly Action closeAction; private Sprite pageIndicator; public static readonly string[] DeliveryTextVariables = new string[] { "[submarinename1]", "[location1]", "[location2]", "[submarinename2]", "[amount]", "[currencyname]" }; @@ -42,7 +43,7 @@ namespace Barotrauma private static readonly string[] notEnoughCreditsDeliveryTextVariables = new string[] { "[currencyname]", "[submarinename]", "[location1]", "[location2]" }; private static readonly string[] notEnoughCreditsPurchaseTextVariables = new string[] { "[currencyname]", "[submarinename]" }; - private string[] messageBoxOptions; + private readonly string[] messageBoxOptions; public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; @@ -65,7 +66,7 @@ namespace Barotrauma public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) { - if (GameMain.GameSession.Campaign == null) return; + if (GameMain.GameSession.Campaign == null) { return; } transferService = transfer; purchaseService = !transfer; @@ -83,7 +84,7 @@ namespace Barotrauma messageBoxOptions = new string[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; } - if (Submarine.MainSub?.Info == null) return; + if (Submarine.MainSub?.Info == null) { return; } Initialize(); } @@ -184,8 +185,10 @@ namespace Barotrauma for (int i = 0; i < submarineDisplays.Length; i++) { - SubmarineDisplayContent submarineDisplayElement = new SubmarineDisplayContent(); - submarineDisplayElement.background = new GUIFrame(new RectTransform(new Vector2(1f / submarinesPerPage, 1f), submarineHorizontalGroup.RectTransform), style: null, new Color(8, 13, 19)); + SubmarineDisplayContent submarineDisplayElement = new SubmarineDisplayContent + { + background = new GUIFrame(new RectTransform(new Vector2(1f / submarinesPerPage, 1f), submarineHorizontalGroup.RectTransform), style: null, new Color(8, 13, 19)) + }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); @@ -433,7 +436,7 @@ namespace Barotrauma private SubmarineInfo GetSubToDisplay(int index) { - if (subsToShow.Count <= index || index < 0) return null; + if (subsToShow.Count <= index || index < 0) { return null; } return subsToShow[index]; } @@ -626,7 +629,6 @@ namespace Barotrauma if (GameMain.Client == null) { SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub); RefreshSubmarineDisplay(true); } else @@ -661,7 +663,6 @@ namespace Barotrauma { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub); RefreshSubmarineDisplay(true); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 3b0679933..a5239b1b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -228,7 +228,10 @@ namespace Barotrauma var crewButton = createTabButton(InfoFrameTab.Crew, "crew"); - var missionButton = createTabButton(InfoFrameTab.Mission, "mission"); + if (!(GameMain.GameSession?.GameMode is TestGameMode)) + { + createTabButton(InfoFrameTab.Mission, "mission"); + } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { @@ -903,51 +906,68 @@ namespace Barotrauma infoFrame.ClearChildren(); GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); int padding = (int)(0.0245f * missionFrame.Rect.Height); - Location location = GameMain.GameSession.EndLocation != null ? GameMain.GameSession.EndLocation : GameMain.GameSession.StartLocation; + GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null); + Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation; + + GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform)) + { + AbsoluteSpacing = GUI.IntScale(10) + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + + var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), + TextManager.Get("Biome", fallBackTag: "location"), font: GUI.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: GUI.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 = hasPortrait ? (int)(missionFrame.Rect.Width * 0.951f) : missionFrame.Rect.Width - padding * 2; - - Vector2 locationNameSize = GUI.LargeFont.MeasureString(location.Name); - Vector2 locationTypeSize = GUI.SubHeadingFont.MeasureString(location.Name); - GUITextBlock locationNameText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationNameSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, location.Name, font: GUI.LargeFont); - GUITextBlock locationTypeText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationTypeSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationNameText.Rect.Height + padding) }, location.Type.Name, font: GUI.SubHeadingFont); - - int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; - - GUIListBox missionList; + int contentWidth = missionFrameContent.Rect.Width; if (hasPortrait) { - GUIFrame portraitHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; - GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), portraitHolder.RectTransform), portrait, scaleToFit: true); - portraitHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); + GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(0.5f, 1f), locationInfoContainer.RectTransform, Anchor.CenterRight), portrait, scaleToFit: true) + { + IgnoreLayoutGroups = true + }; + locationInfoContainer.Recalculate(); + portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y); + } - missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Bottom - portraitHolder.Rect.Bottom - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, portraitHolder.RectTransform.AbsoluteOffset.Y + portraitHolder.Rect.Height + padding) }); - } - else - { - missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Height - locationInfoYOffset - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); - } + 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); if (GameMain.GameSession?.Missions != null) { + int spacing = GUI.IntScale(5); + int iconSize = (int)(GUI.LargeFont.MeasureChar('T').Y + GUI.Font.MeasureChar('T').Y * 4 + spacing * 4); + foreach (Mission mission in GameMain.GameSession.Missions) { 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) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft) + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(iconSize + spacing, 0) }, false, childAnchor: Anchor.TopLeft) { - AbsoluteSpacing = GUI.IntScale(5) + AbsoluteSpacing = spacing }; string descriptionText = mission.Description; foreach (string missionMessage in mission.ShownMessages) { descriptionText += "\n\n" + missionMessage; } - string rewardText = mission.GetMissionRewardText(); + string rewardText = mission.GetMissionRewardText(Submarine.MainSub); string reputationText = mission.GetReputationRewardText(mission.Locations[0]); var missionNameRichTextData = RichTextData.GetRichTextData(mission.Name, out string missionNameString); @@ -974,12 +994,12 @@ namespace Barotrauma if (mission.Prefab.Icon != null) { - float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + /*float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); + Point iconSize = new Point(iconWidth, iconHeight);*/ - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) + new GUIImage(new RectTransform(new Point(iconSize), missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor, HoverColor = mission.Prefab.IconColor, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3b3d5ceff..826ab5f91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -7,8 +9,10 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -using SpriteBatch = Microsoft.Xna.Framework.Graphics.SpriteBatch; + +// ReSharper disable UnusedVariable namespace Barotrauma { @@ -18,8 +22,8 @@ namespace Barotrauma public readonly struct CategoryData { public readonly UpgradeCategory Category; - public readonly List Prefabs; - public readonly UpgradePrefab SinglePrefab; + public readonly List? Prefabs; + public readonly UpgradePrefab? SinglePrefab; public CategoryData(UpgradeCategory category, List prefabs) { @@ -37,30 +41,34 @@ namespace Barotrauma } private readonly CampaignUI campaignUI; - private CampaignMode Campaign => campaignUI?.Campaign; - private UpgradeTab selectedUpgradTab = UpgradeTab.Upgrade; + private CampaignMode? Campaign => campaignUI.Campaign; + private int AvailableMoney => Campaign?.Money ?? 0; + private UpgradeTab selectedUpgradeTab = UpgradeTab.Upgrade; - private GUIMessageBox currectConfirmation; + private GUIMessageBox? currectConfirmation; public readonly GUIFrame ItemInfoFrame; - private GUIComponent selectedUpgradeCategoryLayout; - private GUILayoutGroup topHeaderLayout; - private GUILayoutGroup mainStoreLayout; - private GUILayoutGroup storeLayout; - private GUILayoutGroup categoryButtonLayout; - private GUILayoutGroup submarineInfoFrame; - private GUIListBox currentStoreLayout; - private GUICustomComponent submarinePreviewComponent; - private GUIFrame subPreviewFrame; - private Submarine drawnSubmarine; + private GUIComponent? selectedUpgradeCategoryLayout; + private GUILayoutGroup? topHeaderLayout; + private GUILayoutGroup? mainStoreLayout; + private GUILayoutGroup? storeLayout; + private GUILayoutGroup? categoryButtonLayout; + private GUILayoutGroup? submarineInfoFrame; + private GUIListBox? currentStoreLayout; + private GUICustomComponent? submarinePreviewComponent; + private GUIFrame? subPreviewFrame; + private Submarine? drawnSubmarine; private readonly List applicableCategories = new List(); - private Vector2[][] subHullVerticies = new Vector2[0][]; + private Vector2[][] subHullVertices = new Vector2[0][]; private List submarineWalls = new List(); - public MapEntity HoveredItem; + public MapEntity? HoveredItem; private bool highlightWalls; - private readonly Dictionary itemPreviews = new Dictionary(); + private UpgradeCategory? currentUpgradeCategory; + private GUIButton? activeItemSwapSlideDown; + + private readonly Dictionary itemPreviews = new Dictionary(); private static readonly Color previewWhite = Color.White * 0.5f; @@ -94,6 +102,7 @@ namespace Barotrauma CreateUI(upgradeFrame); + if (Campaign == null) { return; } Campaign.UpgradeManager.OnUpgradesChanged += RefreshAll; Campaign.CargoManager.OnPurchasedItemsChanged += RefreshAll; Campaign.CargoManager.OnSoldItemsChanged += RefreshAll; @@ -101,31 +110,49 @@ namespace Barotrauma public void RefreshAll() { - switch (selectedUpgradTab) + switch (selectedUpgradeTab) { case UpgradeTab.Repairs: - { SelectTab(UpgradeTab.Repairs); break; - } case UpgradeTab.Upgrade: - { RefreshUpgradeList(); + foreach (var itemPreview in itemPreviews) + { + if (!(itemPreview.Value is GUIImage image) || itemPreview.Key == null) { continue; } + if (itemPreview.Key.PendingItemSwap == null) + { + image.Sprite = itemPreview.Key.Prefab.UpgradePreviewSprite; + } + else if (itemPreview.Key.PendingItemSwap.UpgradePreviewSprite != null) + { + image.Sprite = itemPreview.Key.PendingItemSwap.UpgradePreviewSprite; + } + } break; - } } } private void RefreshUpgradeList() { + if (Campaign == null) { return; } // Updates the progress bar / text and disables the buy button if we reached max level if (selectedUpgradeCategoryLayout?.Parent != null && selectedUpgradeCategoryLayout.FindChild("prefablist", true) is GUIListBox listBox) { foreach (var component in listBox.Content.Children) { - if (component.UserData is CategoryData data) + if (component.UserData is CategoryData { SinglePrefab: { } prefab} data) { - UpdateUpgradeEntry(component, data.SinglePrefab, data.Category, Campaign); + UpdateUpgradeEntry(component, prefab, data.Category, Campaign); + } + } + if (customizeTabOpen && selectedUpgradeCategoryLayout != null && Submarine.MainSub != null && currentUpgradeCategory != null) + { + CreateSwappableItemList(listBox, currentUpgradeCategory, Submarine.MainSub); + if (activeItemSwapSlideDown?.UserData is Item prevOpenedItem) + { + var currentButton = listBox.FindChild(c => c.UserData as Item == prevOpenedItem, recursive: true) as GUIButton; + currentButton?.OnClicked(currentButton, prevOpenedItem); } } } @@ -135,18 +162,25 @@ namespace Barotrauma { UpdateCategoryList(currentStoreLayout, Campaign, drawnSubmarine, applicableCategories); } + } //TODO: move this somewhere else - public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine drawnSubmarine, IEnumerable applicableCategories) + public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine? drawnSubmarine, IEnumerable applicableCategories) { foreach (GUIComponent component in categoryList.Content.Children) { if (!(component.UserData is CategoryData data)) { continue; } - if (component.FindChild("indicators", true) is { } indicators) + if (component.FindChild("indicators", true) is { } indicators && data.Prefabs != null) { + // ReSharper disable once PossibleMultipleEnumeration UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category, campaign, drawnSubmarine, applicableCategories); } + var customizeButton = component.FindChild("customizebutton", true); + if (customizeButton != null) + { + customizeButton.Visible = HasSwappableItems(data.Category); + } } // reset the order first @@ -192,7 +226,7 @@ namespace Barotrauma */ private void CreateUI(GUIComponent parent) { - selectedUpgradTab = UpgradeTab.Upgrade; + selectedUpgradeTab = UpgradeTab.Upgrade; parent.ClearChildren(); ItemInfoFrame.ClearChildren(); @@ -213,7 +247,7 @@ namespace Barotrauma GUILayoutGroup tooltipLayout = new GUILayoutGroup(rectT(0.95f,0.95f, ItemInfoFrame, Anchor.Center)) { Stretch = true }; new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUI.SubHeadingFont) { UserData = "itemname" }; new GUITextBlock(rectT(1, 0, tooltipLayout), TextManager.Get("UpgradeUITooltip.UpgradeListHeader")); - new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, UserData = "upgradelist"}; + new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, SmoothScroll = true, UserData = "upgradelist"}; new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); @@ -236,8 +270,8 @@ namespace Barotrauma 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: GUI.LargeFont); 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 = selectedUpgradTab == UpgradeTab.Upgrade }; - GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradTab == UpgradeTab.Repairs }; + 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 }; /* RIGHT HEADER LAYOUT * |---------------------------------------------------------------------------------------------------| @@ -252,22 +286,22 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUI.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(Campaign.Money, format: true), font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(Campaign.Money, format: true) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => { if (o is UpgradeTab upgradeTab) { - if (upgradeTab != selectedUpgradTab || currentStoreLayout == null || currentStoreLayout.Parent != storeLayout) + if (upgradeTab != selectedUpgradeTab || currentStoreLayout == null || currentStoreLayout.Parent != storeLayout) { - selectedUpgradTab = upgradeTab; - SelectTab(selectedUpgradTab); + selectedUpgradeTab = upgradeTab; + SelectTab(selectedUpgradeTab); storeLayout?.Recalculate(); } - repairButton.Selected = (UpgradeTab) repairButton.UserData == selectedUpgradTab; - upgradeButton.Selected = (UpgradeTab) upgradeButton.UserData == selectedUpgradTab; + repairButton.Selected = (UpgradeTab) repairButton.UserData == selectedUpgradeTab; + upgradeButton.Selected = (UpgradeTab) upgradeButton.UserData == selectedUpgradeTab; return true; } @@ -283,6 +317,12 @@ namespace Barotrauma SelectTab(UpgradeTab.Upgrade); + var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.27f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(GUI.IsFourByThree() ? 0.5f : 0.47f, 0.0f) }, DrawItemSwapPreview) + { + IgnoreLayoutGroups = true, + CanBeFocused = true + }; + #if DEBUG // creates a button that re-creates the UI CreateRefreshButton(); @@ -300,16 +340,45 @@ namespace Barotrauma #endif } + private void DrawItemSwapPreview(SpriteBatch spriteBatch, GUICustomComponent component) + { + var selectedItem = customizeTabOpen ? + activeItemSwapSlideDown?.UserData as Item ?? HoveredItem as Item : + HoveredItem as Item; + if (selectedItem?.Prefab.SwappableItem == null) { return; } + + Sprite schematicsSprite = selectedItem.Prefab.SwappableItem.SchematicSprite; + if (schematicsSprite == null) { return; } + float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y); + Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y); + schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUI.Style.Green, new Vector2(0, schematicsSprite.size.Y / 2), + scale: schematicsScale); + + var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox; + var highlightedElement = swappableItemList?.Content.FindChild(c => c.UserData is ItemPrefab && c.IsParentOf(GUI.MouseOn)) ?? GUI.MouseOn; + ItemPrefab swapTo = highlightedElement?.UserData as ItemPrefab ?? selectedItem.PendingItemSwap; + if (swapTo?.SwappableItem == null) { return; } + Sprite? schematicsSprite2 = swapTo.SwappableItem?.SchematicSprite; + schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUI.Style.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2), + scale: Math.Min(component.Rect.Width / 2 / schematicsSprite2.size.X, component.Rect.Height / schematicsSprite2.size.Y)); + + var arrowSprite = GUI.Style?.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite(); + if (arrowSprite != null) + { + arrowSprite.Draw(spriteBatch, center, scale: GUI.Scale); + } + } + private void SelectTab(UpgradeTab tab) { if (currentStoreLayout != null) { - storeLayout.RemoveChild(currentStoreLayout); + storeLayout?.RemoveChild(currentStoreLayout); } if (selectedUpgradeCategoryLayout != null) { - mainStoreLayout.RemoveChild(selectedUpgradeCategoryLayout); + mainStoreLayout?.RemoveChild(selectedUpgradeCategoryLayout); } switch (tab) @@ -329,8 +398,10 @@ namespace Barotrauma private void CreateRepairsTab() { + if (Campaign == null || storeLayout == null) { return; } + highlightWalls = false; - foreach (GUIFrame itemFrame in itemPreviews.Values) + foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = previewWhite; } @@ -355,12 +426,12 @@ namespace Barotrauma return false; } - if (Campaign.Money >= hullRepairCost) + if (AvailableMoney >= hullRepairCost) { string body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (Campaign.Money >= hullRepairCost) + if (AvailableMoney >= hullRepairCost) { Campaign.Money -= hullRepairCost; Campaign.PurchasedHullRepairs = true; @@ -389,12 +460,12 @@ namespace Barotrauma CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => { - if (Campaign.Money >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { string body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (Campaign.Money >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { Campaign.Money -= itemRepairCost; Campaign.PurchasedItemRepairs = true; @@ -434,12 +505,12 @@ namespace Barotrauma return false; } - if (Campaign.Money >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { string body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (Campaign.Money >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { Campaign.Money -= shuttleRetrieveCost; Campaign.PurchasedLostShuttles = true; @@ -460,12 +531,13 @@ namespace Barotrauma }, Campaign.PurchasedLostShuttles || !HasPermission || GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind, isHovered => { if (!isHovered) { return false; } + if (!(GameMain.GameSession?.SubmarineInfo is { } subInfo)) { return false; } foreach (var (item, itemFrame) in itemPreviews) { - if (GameMain.GameSession.SubmarineInfo.LeftBehindDockingPortIDs.Contains(item.ID)) + if (subInfo.LeftBehindDockingPortIDs.Contains(item.ID)) { - itemFrame.OutlineColor = itemFrame.Color = GameMain.GameSession.SubmarineInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUI.Style.Red : GUI.Style.Green; + itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUI.Style.Red : GUI.Style.Green; } else { @@ -476,7 +548,7 @@ namespace Barotrauma }, disableElement: true); } - private void CreateRepairEntry(GUIComponent parent, string title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func onHover = null, bool disableElement = false) + private void CreateRepairEntry(GUIComponent parent, string title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func? onHover = null, bool disableElement = false) { GUIFrame frameChild = new GUIFrame(rectT(new Point(parent.Rect.Width, (int) (96 * GUI.Scale)), parent), style: "UpgradeUIFrame"); frameChild.SelectedColor = frameChild.Color; @@ -497,13 +569,13 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUI.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = "buybutton" }; - new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = Campaign.Money >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = AvailableMoney >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); if (disableElement) { - frameChild.Enabled = Campaign.Money >= price && !isDisabled; + frameChild.Enabled = AvailableMoney >= price && !isDisabled; } if (!HasPermission) @@ -564,6 +636,20 @@ namespace Barotrauma frameChild.DefaultColor = frameChild.Color; frameChild.Color = Color.Transparent; + var weaponSwitchBg = new GUIButton(new RectTransform(new Vector2(0.65f), frameChild.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.Smallest) + { RelativeOffset = new Vector2(0.04f, 0.0f) }, style: "WeaponSwitchTab") + { + Visible = false, + CanBeSelected = false, + UserData = "customizebutton" + }; + weaponSwitchBg.DefaultColor = weaponSwitchBg.Frame.DefaultColor = weaponSwitchBg.Color; + var weaponSwitchImg = new GUIImage(new RectTransform(new Vector2(0.7f), weaponSwitchBg.RectTransform, Anchor.Center), "WeaponSwitchIcon", scaleToFit: true) + { + CanBeFocused = false + }; + weaponSwitchImg.DefaultColor = weaponSwitchImg.Color; + /* UPGRADE CATEGORY * |--------------------------------------------------------| * | | @@ -594,6 +680,7 @@ namespace Barotrauma private void CreateUpgradeTab() { + if (storeLayout == null || mainStoreLayout == null) { return; } currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout)); selectedUpgradeCategoryLayout = new GUIFrame(rectT(GUI.IsFourByThree() ? 0.3f : 0.25f, 1, mainStoreLayout), style: null) { CanBeFocused = false }; @@ -605,18 +692,22 @@ namespace Barotrauma if (!component.Enabled) { selectedUpgradeCategoryLayout?.ClearChildren(); - foreach (GUIFrame itemFrame in itemPreviews.Values) + foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } return true; } - if (userData is CategoryData categoryData && Submarine.MainSub != null) + if (userData is CategoryData categoryData && Submarine.MainSub is { } sub && categoryData.Prefabs is { } prefabs) { - TrySelectCategory(categoryData.Prefabs, categoryData.Category, Submarine.MainSub); + TrySelectCategory(prefabs, categoryData.Category, sub); } + var customizeCategoryButton = selectedUpgradeCategoryLayout?.FindChild("customizebutton", recursive: true) as GUIButton; + customizeCategoryButton?.OnClicked(customizeCategoryButton, customizeCategoryButton.UserData); + return true; }; } @@ -624,35 +715,314 @@ namespace Barotrauma // This was supposed to have some logic for fancy animations to slide the previous tab out but maybe another time private void TrySelectCategory(List prefabs, UpgradeCategory category, Submarine submarine) => SelectUpgradeCategory(prefabs, category, submarine); + private bool customizeTabOpen; + + private static bool HasSwappableItems(UpgradeCategory category) + { + if (Submarine.MainSub == null) { return false; } + return Submarine.MainSub.GetItems(true).Any(i => + i.Prefab.SwappableItem != null && + !i.HiddenInGame && i.AllowSwapping && + (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && + Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); + } + private void SelectUpgradeCategory(List prefabs, UpgradeCategory category, Submarine submarine) { - if (selectedUpgradeCategoryLayout == null || submarine == null) { return; } + if (selectedUpgradeCategoryLayout == null) { return; } - GUIFrame[] categoryFrames = GetFrames(category); - foreach (GUIFrame itemFrame in itemPreviews.Values) + customizeTabOpen = false; + + GUIComponent[] categoryFrames = GetFrames(category); + foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } highlightWalls = category.IsWallUpgrade; - selectedUpgradeCategoryLayout?.ClearChildren(); - GUIFrame frame = new GUIFrame(rectT(1, 0.4f, selectedUpgradeCategoryLayout)); - GUIListBox prefabList = new GUIListBox(rectT(0.93f, 0.9f, frame, Anchor.Center)) { UserData = "prefablist" }; + selectedUpgradeCategoryLayout.ClearChildren(); + GUIFrame frame = new GUIFrame(rectT(1.0f, 0.4f, selectedUpgradeCategoryLayout)); + GUIFrame paddedFrame = new GUIFrame(rectT(0.93f, 0.9f, frame, Anchor.Center), style: null); - List entitiesOnSub = null; + bool hasSwappableItems = HasSwappableItems(category); + + float listHeight = hasSwappableItems ? 0.9f : 1.0f; + + GUIListBox prefabList = new GUIListBox(rectT(1.0f, listHeight, paddedFrame, Anchor.BottomLeft)) + { + UserData = "prefablist", + AutoHideScrollBar = false, + ScrollBarVisible = true + }; + + if (hasSwappableItems) + { + GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1.0f, 0.1f, paddedFrame, anchor: Anchor.TopLeft), isHorizontal: true); + + GUIButton customizeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.customize"), style: "GUITabButton") + { + UserData = "customizebutton" + }; + new GUIImage(new RectTransform(new Vector2(1.0f, 0.75f), customizeButton.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(0.015f, 0.0f) }, "WeaponSwitchIcon", scaleToFit: true); + customizeButton.TextBlock.RectTransform.RelativeSize = new Vector2(0.7f, 1.0f); + + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.upgrades"), style: "GUITabButton") + { + Selected = true + }; + + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, customizeButton.TextBlock); + + upgradeButton.OnClicked = delegate + { + customizeTabOpen = false; + customizeButton.Selected = false; + upgradeButton.Selected = true; + CreateUpgradePrefabList(prefabList, category, prefabs, submarine); + GUIComponent[] categoryFrames = GetFrames(category); + foreach (GUIComponent itemFrame in itemPreviews.Values) + { + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); + } + return true; + }; + + customizeButton.OnClicked = delegate + { + customizeTabOpen = true; + customizeButton.Selected = true; + upgradeButton.Selected = false; + CreateSwappableItemList(prefabList, category, submarine); + return true; + }; + } + + CreateUpgradePrefabList(prefabList, category, prefabs, submarine); + } + + private void CreateUpgradePrefabList(GUIListBox parent, UpgradeCategory category, List prefabs, Submarine submarine) + { + parent.Content.ClearChildren(); + List? entitiesOnSub = null; if (!category.IsWallUpgrade) { entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true)).ToList(); } + foreach (UpgradePrefab prefab in prefabs) { - CreateUpgradeEntry(prefab, category, prefabList.Content, entitiesOnSub); + CreateUpgradeEntry(prefab, category, parent.Content, entitiesOnSub); } } + private void CreateSwappableItemList(GUIListBox parent, UpgradeCategory category, Submarine submarine) + { + parent.Content.ClearChildren(); + currentUpgradeCategory = category; + var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.HiddenInGame && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList(); + + int slotIndex = 0; + foreach (Item item in entitiesOnSub) + { + slotIndex++; + CreateSwappableItemSlideDown(parent, slotIndex, item, submarine); + } + } + + private void CreateSwappableItemSlideDown(GUIListBox parent, int slotIndex, Item item, Submarine submarine) + { + if (Campaign == null || submarine == null) { return; } + + IEnumerable availableReplacements = MapEntityPrefab.List.Where(p => + p is ItemPrefab itemPrefab && + itemPrefab.SwappableItem != null && + itemPrefab.SwappableItem.CanBeBought && + itemPrefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)).Cast(); + + var currentOrPending = item.PendingItemSwap ?? item.Prefab; + + bool isOpen = false; + GUIButton toggleButton = new GUIButton(rectT(1f, 0.1f, parent.Content), text: string.Empty, style: "SlideDown") + { + UserData = item + }; + GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true); + new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: TextManager.GetWithVariable("weaponslot", "[number]", slotIndex.ToString()), font: GUI.SubHeadingFont); + GUILayoutGroup group = new GUILayoutGroup(rectT(0.7f, 1f, buttonLayout), isHorizontal: true) { Stretch = true }; + + string title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", currentOrPending.Name) : item.Name; + GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: title, font: GUI.SubHeadingFont, textAlignment: Alignment.Right, parseRichText: true) + { + TextColor = GUI.Style.Orange + }; + GUIImage arrowImage = new GUIImage(rectT(0.5f, 1f, group, scaleBasis: ScaleBasis.BothHeight), style: "SlideDownArrow", scaleToFit: true); + + group.Recalculate(); + if (text.TextSize.X > text.Rect.Width) + { + text.ToolTip = text.Text; + text.Text = ToolBox.LimitString(text.Text, text.Font, text.Rect.Width); + } + + List frames = new List(); + if (currentOrPending != null) + { + bool canUninstall = item.PendingItemSwap != null || !string.IsNullOrEmpty(currentOrPending.SwappableItem?.ReplacementOnUninstall); + + bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; + if (isUninstallPending) { canUninstall = false; } + + frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, + TextManager.GetWithVariable(item.PendingItemSwap != null ? "upgrades.pendingitem" : "upgrades.installeditem", "[itemname]", currentOrPending.Name), + currentOrPending.Description, + 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); + + if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton) + { + refundButton.Enabled = true; + refundButton.OnClicked += (button, o) => + { + string textTag = item.PendingItemSwap != null ? "upgrades.cancelitemswappromptbody" : "upgrades.itemuninstallpromptbody"; + if (isUninstallPending) { textTag = "upgrades.cancelitemuninstallpromptbody"; } + string promptBody = TextManager.GetWithVariables(textTag, + new[] { "[itemtouninstall]" }, + new[] { isUninstallPending ? item.Name : currentOrPending.Name }); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("upgrades.refundprompttitle"), promptBody, () => + { + if (GameMain.NetworkMember != null) + { + WaitForServerUpdate = true; + } + Campaign?.UpgradeManager.CancelItemSwap(item); + GameMain.Client?.SendCampaignState(); + return true; + }); + return true; + }; + } + + var dividerContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.Content.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(0.8f, 0.5f), dividerContainer.RectTransform, Anchor.Center), style: "HorizontalLine"); + frames.Add(dividerContainer); + } + + foreach (ItemPrefab replacement in availableReplacements) + { + if (replacement == currentOrPending) { continue; } + + bool isPurchased = item.AvailableSwaps.Contains(replacement); + + int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation); + + frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description, + price, replacement, + addBuyButton: true, + addProgressBar: false, + buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); + + if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } + if (Campaign.Money >= price) + { + buyButton.Enabled = true; + buyButton.OnClicked += (button, o) => + { + string promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", + new[] { "[itemtoinstall]", "[amount]" }, + new[] { replacement.Name, replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation).ToString() }); + currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => + { + if (GameMain.NetworkMember != null) + { + WaitForServerUpdate = true; + } + if (item.Prefab == replacement && item.PendingItemSwap != null) + { + Campaign?.UpgradeManager.CancelItemSwap(item); + } + else + { + Campaign?.UpgradeManager.PurchaseItemSwap(item, replacement); + } + GameMain.Client?.SendCampaignState(); + return true; + }); + + return true; + }; + } + else + { + buyButton.Enabled = false; + } + } + + foreach (GUIFrame frame in frames) + { + frame.Visible = false; + } + + toggleButton.OnClicked = delegate + { + isOpen = !isOpen; + toggleButton.Selected = !toggleButton.Selected; + foreach (GUIFrame frame in frames) + { + frame.Visible = toggleButton.Selected; + } + if (toggleButton.Selected) + { + foreach (var itemPreview in itemPreviews) + { + itemPreview.Value.OutlineColor = itemPreview.Value.Color = itemPreview.Key == item ? GUI.Style.Orange : previewWhite; + } + foreach (GUIComponent otherComponent in toggleButton.Parent.Children) + { + if (otherComponent == toggleButton || frames.Contains(otherComponent)) { continue; } + if (otherComponent is GUIButton otherButton) + { + var otherArrowImage = otherComponent.FindChild(c => c is GUIImage, recursive: true); + otherArrowImage.SpriteEffects = SpriteEffects.None; + otherButton.Selected = false; + } + else + { + otherComponent.Visible = false; + } + } + } + else + { + foreach (var itemPreview in itemPreviews) + { + if (currentStoreLayout?.SelectedData is CategoryData categoryData && !categoryData.Category.ItemTags.Any(t => itemPreview.Key.HasTag(t))) { continue; } + itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUI.Style.Orange; + } + } + activeItemSwapSlideDown = toggleButton.Selected ? toggleButton : null; + arrowImage.SpriteEffects = toggleButton.Selected ? SpriteEffects.FlipVertically : SpriteEffects.None; + parent.RecalculateChildren(); + parent.UpdateScrollBarSize(); + return true; + }; + } + public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true) { + int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation); + return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton); + } + + public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, string title, string body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton") + { + float progressBarHeight = 0.25f; + + if (!addProgressBar) + { + progressBarHeight = 0f; + } /* UPGRADE PREFAB ENTRY * |------------------------------------------------------------------| * | | title | price | @@ -662,23 +1032,40 @@ namespace Barotrauma * | | progress bar | x / y | | * |------------------------------------------------------------------| */ - GUIFrame prefabFrame = new GUIFrame(rectTransform, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = new CategoryData(category, prefab) }; + GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData }; GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: true) { Stretch = true }; GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); - var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout), prefab.Sprite, scaleToFit: true) { CanBeFocused = false }; + var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), prefab.Name, font: GUI.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; - GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.50f, textLayout)); - var description = new GUITextBlock(rectT(1, 1, descriptionLayout), prefab.Description, font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; - GUILayoutGroup progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; - new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUI.Style.Orange); - new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUI.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; - GUILayoutGroup buyButtonLayout = null; + var name = new GUITextBlock(rectT(1, 0.25f, textLayout), title, font: GUI.SubHeadingFont, parseRichText: true) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + GUILayoutGroup? progressLayout = null; + GUILayoutGroup? buyButtonLayout = null; + + if (addProgressBar) + { + progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; + new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUI.Style.Orange); + new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUI.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + } + if (addBuyButton) { - buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; - new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), FormatCurrency(prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation)), textAlignment: Alignment.Center) { Padding = Vector4.Zero }; - var buyButton = new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "UpgradeBuyButton") { Enabled = false }; + string formattedPrice = FormatCurrency(Math.Abs(price)); + //negative price = refund + if (price < 0) { formattedPrice = "+" + formattedPrice; } + buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; + var priceText = new GUITextBlock(rectT(1, 0.4f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); + if (price < 0) + { + priceText.TextColor = GUI.Style.Green; + } + else if (price == 0) + { + priceText.Text = string.Empty; + } + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: buttonStyle) { Enabled = false }; } description.CalculateHeightFromText(); @@ -691,24 +1078,26 @@ namespace Barotrauma description.Text = newString.Substring(0, newString.Length - 4) + "..."; description.CalculateHeightFromText(); - description.ToolTip = prefab.Description; + description.ToolTip = body; } // Recalculate everything to prevent jumping - if (rectTransform.Parent.GUIComponent is GUILayoutGroup group) { group.Recalculate(); } + if (parent.Parent.GUIComponent is GUILayoutGroup group) { group.Recalculate(); } descriptionLayout.Recalculate(); prefabLayout.Recalculate(); imageLayout.Recalculate(); textLayout.Recalculate(); - progressLayout.Recalculate(); + progressLayout?.Recalculate(); buyButtonLayout?.Recalculate(); return prefabFrame; } - private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List itemsOnSubmarine) + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List? itemsOnSubmarine) { + if (Campaign is null) { return; } + GUIFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.25f, parent)); var prefabLayout = prefabFrame.GetChild(); GUILayoutGroup[] childLayouts = prefabLayout.GetAllChildren().ToArray(); @@ -723,7 +1112,7 @@ namespace Barotrauma var buyButtonLayout = childLayouts[2]; var buyButton = buyButtonLayout.GetChild(); - if (!HasPermission || itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))) + if (!HasPermission || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab)))) { prefabFrame.Enabled = false; description.Enabled = false; @@ -755,9 +1144,16 @@ namespace Barotrauma private void CreateItemTooltip(MapEntity entity) { - GUITextBlock itemName = ItemInfoFrame.FindChild("itemname", true) as GUITextBlock; - GUIListBox upgradeList = ItemInfoFrame.FindChild("upgradelist", true) as GUIListBox; - GUITextBlock moreIndicator = ItemInfoFrame.FindChild("moreindicator", true) as GUITextBlock; + int slotIndex = -1; + if (currentStoreLayout?.SelectedData is CategoryData categoryData) + { + var entitiesOnSub = Submarine.MainSub.GetItems(true).Where(i => i.Prefab.SwappableItem != null && Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && categoryData.Category.ItemTags.Any(t => i.HasTag(t))).ToList(); + slotIndex = entitiesOnSub.IndexOf(entity) + 1; + } + + GUITextBlock? itemName = ItemInfoFrame.FindChild("itemname", true) as GUITextBlock; + GUIListBox? upgradeList = ItemInfoFrame.FindChild("upgradelist", true) as GUIListBox; + GUITextBlock? moreIndicator = ItemInfoFrame.FindChild("moreindicator", true) as GUITextBlock; GUILayoutGroup layout = ItemInfoFrame.GetChild(); Debug.Assert(itemName != null && upgradeList != null && moreIndicator != null && layout != null, "One ore more tooltip elements not found"); @@ -766,6 +1162,10 @@ namespace Barotrauma const int maxUpgrades = 4; itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); + if (slotIndex > -1) + { + itemName.Text = TextManager.GetWithVariables("weaponslotwithname", new string[] { "[number]", "[weaponname]" }, new string[] { slotIndex.ToString(), itemName.Text }); + } upgradeList.Content.ClearChildren(); for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) { @@ -773,8 +1173,10 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), CreateListEntry(upgrade.Prefab.Name, upgrade.Level)) { AutoScaleHorizontal = true, UserData = Tuple.Create(upgrade.Level, upgrade.Prefab) }; } + if (!(Campaign?.UpgradeManager is { } upgradeManager)) { return; } + // include pending upgrades into the tooltip - foreach (var (prefab, category, level) in Campaign.UpgradeManager.PendingUpgrades) + foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades) { if (entity is Item item && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) { @@ -856,16 +1258,14 @@ namespace Barotrauma if (GUIMessageBox.MessageBoxes[i] is GUIMessageBox msgBox && msgBox == currectConfirmation) { // first button is the ok button - GUIButton firstButton = msgBox.Buttons.FirstOrDefault(); - if (firstButton == null) { continue; } + GUIButton? firstButton = msgBox.Buttons.FirstOrDefault(); + if (firstButton is null) { continue; } firstButton.OnClicked.Invoke(firstButton, firstButton.UserData); } } } - if (itemPreviews == null) { return; } - bool found = false; foreach (var (item, frame) in itemPreviews) { @@ -873,9 +1273,23 @@ namespace Barotrauma { if (HoveredItem != item) { CreateItemTooltip(item); } HoveredItem = item; - if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == UpgradeTab.Upgrade && currentStoreLayout != null) + if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { - ScrollToCategory(data => data.Category.CanBeApplied(item, null)); + if (customizeTabOpen) + { + if (selectedUpgradeCategoryLayout != null) + { + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem, recursive: true) is GUIButton itemElement) + { + if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } + (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); + } + } + } + else + { + ScrollToCategory(data => data.Category.CanBeApplied(item, null)); + } } found = true; break; @@ -888,16 +1302,16 @@ namespace Barotrauma if (GUI.MouseOn == submarinePreviewComponent || GUI.MouseOn == subPreviewFrame) { // Every wall should have the same upgrades so we can just display the first one in the tooltip - Structure firstStructure = submarineWalls.FirstOrDefault(); + Structure? firstStructure = submarineWalls.FirstOrDefault(); // use pnpoly algorithm to detect if our mouse is within any of the hull polygons - if (subHullVerticies.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex))) + if (subHullVertices.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex))) { - if (HoveredItem != firstStructure) { CreateItemTooltip(firstStructure); } + if (HoveredItem != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } HoveredItem = firstStructure; isMouseOnStructure = true; GUI.MouseCursor = CursorState.Hand; - if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == UpgradeTab.Upgrade && currentStoreLayout != null) + if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { ScrollToCategory(data => data.Category.IsWallUpgrade); } @@ -917,6 +1331,8 @@ namespace Barotrauma private void CreateSubmarinePreview(Submarine submarine, GUIComponent parent) { + if (mainStoreLayout == null) { return; } + if (submarineInfoFrame != null && mainStoreLayout == submarineInfoFrame.Parent) { mainStoreLayout.RemoveChild(submarineInfoFrame); @@ -961,15 +1377,41 @@ namespace Barotrauma GUIComponent component = parent.FindChild(entity, true); if (component != null && entity is Item item) { - Point size = new Point((int) (item.Rect.Width * item.Scale / dockedBorders.Width * hullContainer.Rect.Width), (int) (item.Rect.Height * item.Scale / dockedBorders.Height * hullContainer.Rect.Height)); - GUIFrame itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines") + GUIComponent itemFrame; + if (item.Prefab.UpgradePreviewSprite is { } icon) { - SelectedColor = GUI.Style.Orange, - OutlineColor = previewWhite, - Color = previewWhite, - OutlineThickness = 2, - HoverCursor = CursorState.Hand - }; + float spriteSize = 128f * item.Prefab.UpgradePreviewScale; + Point size = new Point((int) (spriteSize * item.Scale / dockedBorders.Width * hullContainer.Rect.Width)); + itemFrame = new GUIImage(rectT(size, component, Anchor.Center), icon, scaleToFit: true) + { + SelectedColor = GUI.Style.Orange, + Color = previewWhite, + HoverCursor = CursorState.Hand, + SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None + }; + if (item.Prefab.SwappableItem != null) + { + new GUIImage(new RectTransform(new Vector2(0.8f), itemFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(-0.2f) }, "WeaponSwitchIcon.DropShadow", scaleToFit: true) + { + SelectedColor = GUI.Style.Orange, + Color = previewWhite, + CanBeFocused = false + }; + } + } + else + { + Point size = new Point((int) (item.Rect.Width * item.Scale / dockedBorders.Width * hullContainer.Rect.Width), (int) (item.Rect.Height * item.Scale / dockedBorders.Height * hullContainer.Rect.Height)); + itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines") + { + SelectedColor = GUI.Style.Orange, + OutlineColor = previewWhite, + Color = previewWhite, + OutlineThickness = 2, + HoverCursor = CursorState.Hand + }; + } + if (!itemPreviews.ContainsKey(item)) { itemPreviews.Add(item, itemFrame); @@ -1003,7 +1445,7 @@ namespace Barotrauma Vector2 offset = (sub.WorldPosition - new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)) * scale; Vector2 center = parent.Rect.Center.ToVector2(); - subHullVerticies = new Vector2[sub.HullVertices.Count][]; + subHullVertices = new Vector2[sub.HullVertices.Count][]; for (int i = 0; i < sub.HullVertices.Count; i++) { @@ -1017,7 +1459,7 @@ namespace Barotrauma float angle = (float)Math.Atan2(edge.Y, edge.X); Matrix rotate = Matrix.CreateRotationZ(angle); - subHullVerticies[i] = new[] + subHullVertices[i] = new[] { center + start + Vector2.Transform(new Vector2(length, -lineWidth), rotate), center + end + Vector2.Transform(new Vector2(-length, -lineWidth), rotate), @@ -1029,7 +1471,7 @@ namespace Barotrauma private void DrawSubmarine(SpriteBatch spriteBatch, GUICustomComponent component) { - foreach (Vector2[] hullVertex in subHullVerticies) + foreach (Vector2[] hullVertex in subHullVertices) { // calculate the center point so we can draw a line from X to Y instead of drawing a rotated rectangle that is filled Vector2 point1 = hullVertex[1] + (hullVertex[2] - hullVertex[1]) / 2; @@ -1093,7 +1535,7 @@ namespace Barotrauma List prefabs, UpgradeCategory category, CampaignMode campaign, - Submarine drawnSubmarine, + Submarine? drawnSubmarine, IEnumerable applicableCategories) { // Disables the parent and only re-enables if the submarine contains valid items @@ -1148,6 +1590,8 @@ namespace Barotrauma private void ScrollToCategory(Predicate predicate) { + if (currentStoreLayout == null) { return; } + foreach (GUIComponent child in currentStoreLayout.Content.Children) { if (child.UserData is CategoryData data && predicate(data)) @@ -1163,9 +1607,9 @@ namespace Barotrauma /// /// /// - private GUIFrame[] GetFrames(UpgradeCategory category) + private GUIComponent[] GetFrames(UpgradeCategory category) { - List frames = new List(); + List frames = new List(); foreach (var (item, guiFrame) in itemPreviews) { if (category.CanBeApplied(item, null)) @@ -1185,9 +1629,9 @@ namespace Barotrauma } // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me) - private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft) + private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal) { - return new RectTransform(new Vector2(x, y), parentComponent.RectTransform, anchor); + return new RectTransform(new Vector2(x, y), parentComponent.RectTransform, anchor, scaleBasis: scaleBasis); } private static RectTransform rectT(Point point, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 53096b921..0dbbe81f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -26,6 +26,7 @@ namespace Barotrauma public static bool ShowFPS = false; public static bool ShowPerf = false; public static bool DebugDraw; + public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; public static PerformanceCounter PerformanceCounter; @@ -245,6 +246,23 @@ namespace Barotrauma FarseerPhysics.Settings.PositionIterations = 1; MainThread = Thread.CurrentThread; + + Window.FileDropped += OnFileDropped; + } + + public static void OnFileDropped(object sender, FileDropEventArgs args) + { + if (!(Screen.Selected is { } screen)) { return; } + + string filePath = args.FilePath; + if (string.IsNullOrWhiteSpace(filePath)) { return; } + + string extension = Path.GetExtension(filePath).ToLower(); + + System.IO.FileInfo info = new System.IO.FileInfo(args.FilePath); + if (!info.Exists) { return; } + + screen.OnFileDropped(filePath, extension); } public void ApplyGraphicsSettings() @@ -933,10 +951,7 @@ namespace Barotrauma Screen.Selected.AddToGUIUpdateList(); - if (Client != null) - { - Client.AddToGUIUpdateList(); - } + Client?.AddToGUIUpdateList(); SubmarinePreview.AddToGUIUpdateList(); @@ -966,10 +981,7 @@ namespace Barotrauma } } - if (NetworkMember != null) - { - NetworkMember.Update((float)Timing.Step); - } + NetworkMember?.Update((float)Timing.Step); GUI.Update((float)Timing.Step); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index fba84ab41..cffadc3ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -42,10 +43,7 @@ namespace Barotrauma public IEnumerable GetSellableItems(Character character) { if (character == null) { return new List(); } - // Only consider items which have been: - // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or - // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them - var confirmedSoldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); + var confirmedSoldEntities = GetConfirmedSoldEntities(); // The bag slot is intentionally left out since we want to be able to sell items from there var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; return character.Inventory.FindAllItems(item => @@ -72,6 +70,43 @@ namespace Barotrauma } } + public IEnumerable GetSellableItemsFromSub() + { + if (Submarine.MainSub == null) { return new List(); } + var confirmedSoldEntities = GetConfirmedSoldEntities(); + return Submarine.MainSub.GetItems(true).FindAll(item => + { + if (!item.Prefab.CanBeSold) { return false; } + if (item.SpawnedInOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; } + if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; } + if (!ItemAndAllContainersInteractable(item)) { return false; } + if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } + // There must be no contained items or the contained items must be confirmed as sold + if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + return true; + }).Distinct(); + + static bool ItemAndAllContainersInteractable(Item item) + { + do + { + if (!item.IsPlayerTeamInteractable) { return false; } + item = item.Container; + } while (item != null); + return true; + } + } + + private IEnumerable GetConfirmedSoldEntities() + { + // Only consider items which have been: + // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or + // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them + return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); + } + public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); @@ -119,10 +154,34 @@ namespace Barotrauma OnItemsInSellCrateChanged?.Invoke(); } - public void SellItems(List itemsToSell) + public void ModifyItemQuantityInSellFromSubCrate(ItemPrefab itemPrefab, int changeInQuantity) { - var itemsInInventory = GetSellableItems(Character.Controlled); - var canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; + var itemToSell = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); + if (itemToSell != null) + { + itemToSell.Quantity += changeInQuantity; + if (itemToSell.Quantity < 1) + { + ItemsInSellFromSubCrate.Remove(itemToSell); + } + } + else if (changeInQuantity > 0) + { + itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); + ItemsInSellFromSubCrate.Add(itemToSell); + } + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + + public void SellItems(List itemsToSell, Store.StoreTab sellingMode) + { + var sellableItems = sellingMode switch + { + Store.StoreTab.Sell => GetSellableItems(Character.Controlled), + Store.StoreTab.SellFromSub => GetSellableItemsFromSub(), + _ => throw new System.NotImplementedException(), + }; + bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; var sellerId = GameMain.Client?.ID ?? 0; // Check all the prices before starting the transaction @@ -137,7 +196,7 @@ namespace Barotrauma if (Location.StoreCurrentBalance < itemValue) { continue; } // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) - var matchingItems = itemsInInventory.Where(i => i.Prefab == item.ItemPrefab); + var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab); if (matchingItems.Count() <= item.Quantity) { foreach (Item i in matchingItems) @@ -163,12 +222,21 @@ namespace Barotrauma campaign.Money += itemValue; // Remove from the sell crate - if (ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) + // TODO: Simplify duplicate logic? + if (sellingMode == Store.StoreTab.Sell && ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } inventoryItem) { - itemToSell.Quantity -= item.Quantity; - if (itemToSell.Quantity < 1) + inventoryItem.Quantity -= item.Quantity; + if (inventoryItem.Quantity < 1) { - ItemsInSellCrate.Remove(itemToSell); + ItemsInSellCrate.Remove(inventoryItem); + } + } + else if(sellingMode == Store.StoreTab.SellFromSub && ItemsInSellFromSubCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } subItem) + { + subItem.Quantity -= item.Quantity; + if (subItem.Quantity < 1) + { + ItemsInSellFromSubCrate.Remove(subItem); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index c52aa864f..0e65c1d8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -71,6 +71,7 @@ namespace Barotrauma : this(isSinglePlayer) { AddCharacterElements(element); + ActiveOrdersElement = element.GetChildElement("activeorders"); } partial void InitProjectSpecific() @@ -192,7 +193,8 @@ namespace Barotrauma { AbsoluteSpacing = (int)(5 * GUI.Scale), UserData = "reportbuttons", - CanBeFocused = false + CanBeFocused = false, + Visible = false }; ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(0, -chatBox.ToggleButton.Rect.Height); @@ -433,13 +435,20 @@ namespace Barotrauma Stretch = false }; - var soundIcons = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) + var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, - UserData = "soundicons" + UserData = "extraicons" + }; + + var soundIconParent = new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) + { + CanBeFocused = false, + UserData = "soundicons", + Visible = character.IsPlayer }; new GUIImage( - new RectTransform(Vector2.One, soundIcons.RectTransform), + new RectTransform(Vector2.One, soundIconParent.RectTransform), GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { @@ -448,7 +457,7 @@ namespace Barotrauma Visible = true }; new GUIImage( - new RectTransform(Vector2.One, soundIcons.RectTransform), + new RectTransform(Vector2.One, soundIconParent.RectTransform), "GUISoundIconDisabled", scaleToFit: true) { @@ -457,6 +466,16 @@ namespace Barotrauma Visible = false }; + if (character.IsBot) + { + new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform), style: null) + { + CanBeFocused = false, + UserData = "objectiveicon", + Visible = false + }; + } + new GUIButton(new RectTransform(new Point((int)commandButtonAbsoluteHeight), background.RectTransform), style: "CrewListCommandButton") { ToolTip = TextManager.Get("inputtype.command"), @@ -624,9 +643,7 @@ namespace Barotrauma { if (client?.Character == null) { return; } - if (crewList.Content.GetChildByUserData(client.Character)? - .FindChild(c => c is GUILayoutGroup)? - .GetChildByUserData("soundicons") is GUIComponent soundIcons) + if (GetSoundIconParent(client.Character) is GUIComponent soundIcons) { var soundIcon = soundIcons.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); var soundIconDisabled = soundIcons.FindChild("soundicondisabled"); @@ -638,15 +655,17 @@ namespace Barotrauma public void SetClientSpeaking(Client client) { - if (client?.Character != null) { SetCharacterSpeaking(client.Character); } + if (client?.Character != null) + { + SetCharacterSpeaking(client.Character); + } } public void SetCharacterSpeaking(Character character) { - if (crewList.Content.GetChildByUserData(character)? - .FindChild(c => c is GUILayoutGroup)? - .GetChildByUserData("soundicons")? - .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIComponent soundIcon) + if (character == null || character.IsBot) { return; } + + if (GetSoundIconParent(character)?.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIComponent soundIcon) { soundIcon.Color = Color.White; Pair userdata = soundIcon.UserData as Pair; @@ -654,6 +673,19 @@ namespace Barotrauma } } + private GUIComponent GetSoundIconParent(GUIComponent characterComponent) + { + return characterComponent? + .FindChild(c => c is GUILayoutGroup)? + .GetChildByUserData("extraicons")? + .GetChildByUserData("soundicons"); + } + + private GUIComponent GetSoundIconParent(Character character) + { + return GetSoundIconParent(crewList?.Content.GetChildByUserData(character)); + } + #endregion #region Crew List Order Displayment @@ -770,6 +802,7 @@ namespace Barotrauma if (icon is GUIImage image) { image.Sprite = GetOrderIconSprite(order, option); + image.ToolTip = CreateOrderTooltip(order, option); } updatedExistingIcon = true; } @@ -835,7 +868,7 @@ namespace Barotrauma Visible = false }; - int hierarchyIndex = GetOrderIconHierarchyIndex(priority); + int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - priority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0)); if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon)) { nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex); @@ -878,11 +911,6 @@ namespace Barotrauma } } } - - static int GetOrderIconHierarchyIndex(int priority) - { - return CharacterInfo.HighestManualOrderPriority - priority; - } } public void AddCurrentOrderIcon(Character character, OrderInfo? orderInfo) @@ -1020,29 +1048,36 @@ namespace Barotrauma SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); } - private string CreateOrderTooltip(Order order, string option) + private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) { - if (order == null) { return ""; } + if (orderPrefab == null) { return ""; } if (!string.IsNullOrEmpty(option)) { return TextManager.GetWithVariables("crewlistordericontooltip", new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { order.Name, order.GetOptionName(option) }); + new string[2] { orderPrefab.Name, orderPrefab.GetOptionName(option) }); } - else if (order.TargetEntity is Item targetItem && order.MinimapIcons.ContainsKey(targetItem.Prefab.Identifier)) + else if (targetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { return TextManager.GetWithVariables("crewlistordericontooltip", new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { order.Name, targetItem.Name }); + new string[2] { orderPrefab.Name, targetItem.Name }); } else { - return order.Name; + return orderPrefab.Name; } } - private string CreateOrderTooltip(OrderInfo orderInfo) => - CreateOrderTooltip(orderInfo.Order, orderInfo.OrderOption); + private string CreateOrderTooltip(Order order, string option) + { + return CreateOrderTooltip(order?.Prefab ?? order, option, order?.TargetEntity); + } + + private string CreateOrderTooltip(OrderInfo orderInfo) + { + return CreateOrderTooltip(orderInfo.Order?.Prefab ?? orderInfo.Order, orderInfo.OrderOption, orderInfo.Order?.TargetEntity); + } private Sprite GetOrderIconSprite(Order order, string option) { @@ -1052,9 +1087,9 @@ namespace Barotrauma { order.Prefab.OptionSprites.TryGetValue(option, out sprite); } - if (sprite == null && order.TargetEntity is Item targetItem && order.MinimapIcons.Any()) + if (sprite == null && order.TargetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { - order.MinimapIcons.TryGetValue(targetItem.Prefab.Identifier, out sprite); + sprite = targetItem.Prefab.MinimapIcon; } return sprite ?? order.SymbolSprite; } @@ -1244,10 +1279,7 @@ namespace Barotrauma //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) var aiController = Character.Controlled?.AIController; - if (aiController != null) - { - aiController.Reset(); - } + aiController?.Reset(); DisableCommandUI(); Character.Controlled = character; HintManager.OnChangeCharacter(); @@ -1255,24 +1287,34 @@ namespace Barotrauma private int TryAdjustIndex(int amount) { - int index = Character.Controlled == null ? 0 : - crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled)) + amount; + if (Character.Controlled == null) { return 0; } + + int currentIndex = crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled)); + if (currentIndex == -1) { return 0; } + int lastIndex = crewList.Content.CountChildren - 1; - if (index > lastIndex) + + int index = currentIndex + amount; + for (int i = 0; i < crewList.Content.CountChildren; i++) { - index = 0; + if (index > lastIndex) { index = 0; } + if (index < 0) { index = lastIndex; } + + if ((crewList.Content.GetChild(index)?.UserData as Character)?.IsOnPlayerTeam ?? false) + { + return index; + } + + index += amount; } - if (index < 0) - { - index = lastIndex; - } - return index; + + return 0; } partial void UpdateProjectSpecific(float deltaTime) { // Quick selection - if (!GameMain.IsMultiplayer && GUI.KeyboardDispatcher.Subscriber == null) + if (GameMain.IsSingleplayer && GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(InputType.SelectNextCharacter)) { @@ -1294,7 +1336,7 @@ namespace Barotrauma (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false)) { - if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) + if (PlayerInput.IsShiftDown()) { CreateCommandUI(FindEntityContext(), true); } @@ -1521,19 +1563,44 @@ namespace Barotrauma { crewList.Select(character, force: true); } - if (character.AIController is HumanAIController controller) + if (GameMain.IsSingleplayer && character.IsBot && character.AIController is HumanAIController controller && + controller.ObjectiveManager is AIObjectiveManager objectiveManager) { - OrderInfo? currentOrderInfo = controller.ObjectiveManager?.GetCurrentOrderInfo(); - if (currentOrderInfo.HasValue) + // In multiplayer, these are set through character networking (the server lets the clients now when these are updated) + if (objectiveManager.CurrentObjective is AIObjective currentObjective) { - SetHighlightedOrderIcon(characterComponent, currentOrderInfo.Value.Order?.Identifier, currentOrderInfo.Value.OrderOption); + if (objectiveManager.IsOrder(currentObjective)) + { + var orderInfo = objectiveManager.CurrentOrders.FirstOrDefault(o => o.Objective == currentObjective); + SetOrderHighlight(characterComponent, orderInfo.Order?.Identifier, orderInfo.OrderOption); + } + else + { + CreateObjectiveIcon(characterComponent, currentObjective); + } } } - if (characterComponent.GetChild().GetChildByUserData("soundicons") is GUIComponent soundIconParent) + // Order highlighting and objective icons are intended to communicate bot behavior so they should be disabled for player characters + if (character.IsPlayer) + { + DisableOrderHighlight(characterComponent); + RemoveObjectiveIcon(characterComponent); + } + if (GetSoundIconParent(characterComponent) is GUIComponent soundIconParent) { if (soundIconParent.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { - VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + if (character.IsPlayer) + { + soundIconParent.Visible = true; + VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + } + else if(soundIcon.Visible) + { + var userdata = soundIcon.UserData as Pair; + userdata.Second = 0.0f; + soundIconParent.Visible = soundIcon.Visible = false; + } } } } @@ -1559,31 +1626,127 @@ namespace Barotrauma UpdateReports(); } - private void SetHighlightedOrderIcon(GUIComponent characterComponent, string orderIdentifier, string orderOption) + private void SetOrderHighlight(GUIComponent characterComponent, string orderIdentifier, string orderOption) { - var currentOrderIconList = GetCurrentOrderIconList(characterComponent); - if (currentOrderIconList == null) { return; } - bool foundMatch = false; - foreach (var orderIcon in currentOrderIconList.Content.Children) + if (characterComponent == null) { return; } + RemoveObjectiveIcon(characterComponent); + if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) { - var glowComponent = orderIcon.GetChildByUserData("glow"); - if (glowComponent == null) { continue; } - if (foundMatch) + bool foundMatch = false; + foreach (var orderIcon in currentOrderIconList.Content.Children) { - glowComponent.Visible = false; - continue; + var glowComponent = orderIcon.GetChildByUserData("glow"); + if (glowComponent == null) { continue; } + if (foundMatch) + { + glowComponent.Visible = false; + continue; + } + var orderInfo = (OrderInfo)orderIcon.UserData; + foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); + glowComponent.Visible = foundMatch; } - var orderInfo = (OrderInfo)orderIcon.UserData; - foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); - glowComponent.Visible = foundMatch; } } - public void SetHighlightedOrderIcon(Character character, string orderIdentifier, string orderOption) + public void SetOrderHighlight(Character character, string orderIdentifier, string orderOption) { if (crewList == null) { return; } var characterComponent = crewList.Content.GetChildByUserData(character); - SetHighlightedOrderIcon(characterComponent, orderIdentifier, orderOption); + SetOrderHighlight(characterComponent, orderIdentifier, orderOption); + } + + private void DisableOrderHighlight(GUIComponent characterComponent) + { + if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList) + { + foreach (var orderIcon in currentOrderIconList.Content.Children) + { + var glowComponent = orderIcon.GetChildByUserData("glow"); + if (glowComponent == null) { continue; } + glowComponent.Visible = false; + } + } + } + + private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, string tooltip) + { + if (characterComponent == null || !(characterComponent.UserData is Character character) || character.IsPlayer) { return; } + DisableOrderHighlight(characterComponent); + if (GetObjectiveIconParent(characterComponent) is GUIFrame objectiveIconFrame) + { + var existingObjectiveIcon = objectiveIconFrame.GetChild(); + if (existingObjectiveIcon == null || existingObjectiveIcon.Sprite != sprite || existingObjectiveIcon.ToolTip != tooltip) + { + objectiveIconFrame.ClearChildren(); + if (sprite != null) + { + var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, Color.LightGray, tooltip: tooltip); + new GUIFrame(new RectTransform(new Vector2(1.5f), objectiveIcon.RectTransform, anchor: Anchor.Center), style: "OuterGlowCircular") + { + CanBeFocused = false, + Color = Color.LightGray + }; + objectiveIconFrame.Visible = true; + } + else + { + objectiveIconFrame.Visible = false; + } + } + } + } + + public void CreateObjectiveIcon(Character character, string identifier, string option, Entity targetEntity) + { + CreateObjectiveIcon(crewList?.Content.GetChildByUserData(character), + AIObjective.GetSprite(identifier, option, targetEntity), + GetObjectiveIconTooltip(identifier, option, targetEntity)); + } + + private void CreateObjectiveIcon(GUIComponent characterComponent, AIObjective objective) + { + CreateObjectiveIcon(characterComponent, + objective?.GetSprite(), + GetObjectiveIconTooltip(objective)); + } + + private string GetObjectiveIconTooltip(string identifier, string option, Entity targetEntity) + { + string variableValue; + identifier = identifier.RemoveWhitespace(); + if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + { + variableValue = CreateOrderTooltip(orderPrefab, option, targetEntity); + } + else + { + variableValue = TextManager.Get($"objective.{identifier}", returnNull: true) ?? ""; + } + return string.IsNullOrEmpty(variableValue) ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue); + } + + private string GetObjectiveIconTooltip(AIObjective objective) + { + return objective == null ? "" : + GetObjectiveIconTooltip(objective.Identifier, objective.Option, (objective as AIObjectiveOperateItem)?.OperateTarget); + } + + private GUIComponent GetObjectiveIconParent(GUIComponent characterComponent) + { + return characterComponent? + .GetChild()? + .GetChildByUserData("extraicons")? + .GetChildByUserData("objectiveicon"); + } + + private void RemoveObjectiveIcon(GUIComponent characterComponent) + { + if (GetObjectiveIconParent(characterComponent) is GUIFrame objectiveIconFrame) + { + objectiveIconFrame.ClearChildren(); + objectiveIconFrame.Visible = false; + } } #endregion @@ -1853,7 +2016,10 @@ namespace Barotrauma { nodeConnectors = new GUICustomComponent( new RectTransform(Vector2.One, commandFrame.RectTransform), - onDraw: DrawNodeConnectors); + onDraw: DrawNodeConnectors) + { + CanBeFocused = false + }; nodeConnectors.SetAsFirstChild(); background.SetAsFirstChild(); } @@ -1862,10 +2028,27 @@ namespace Barotrauma { if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); - // Don't draw connectors for mini map options or assignment nodes - if ((targetFrame == null || !targetFrame.Visible) && !(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) + // Don't draw connectors for assignment nodes + if (!(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) { - optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); + // Regular option nodes + if (targetFrame == null || !targetFrame.Visible) + { + optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); + } + // Minimap item nodes for single-option orders + else if(optionNodes.FirstOrDefault()?.Item1?.UserData is Tuple userData && string.IsNullOrEmpty(userData.Item2)) + { + foreach (var node in optionNodes) + { + float iconRadius = 0.5f * optionNodeMargin; + Vector2 itemPosition = node.Item1.Parent.Rect.Center.ToVector2(); + if (Vector2.Distance(node.Item1.Center, itemPosition) <= iconRadius) { continue; } + DrawNodeConnector(itemPosition, 0.0f, node.Item1, iconRadius, spriteBatch, widthMultiplier: 0.5f); + GUI.DrawFilledRectangle(spriteBatch, itemPosition - Vector2.One, new Vector2(3), + node.Item1.GetChildByUserData("colorsource")?.Color ?? Color.White); + } + } } DrawNodeConnector(startNodePos, centerNodeMargin, returnNode, returnNodeMargin, spriteBatch); if (shortcutCenterNode == null || !shortcutCenterNode.Visible) { return; } @@ -1874,7 +2057,7 @@ namespace Barotrauma shortcutNodes.ForEach(n => DrawNodeConnector(startNodePos, shortcutCenterNodeMargin, n, shortcutNodeMargin, spriteBatch)); } - private void DrawNodeConnector(Vector2 startNodePos, float startNodeMargin, GUIComponent endNode, float endNodeMargin, SpriteBatch spriteBatch) + private void DrawNodeConnector(Vector2 startNodePos, float startNodeMargin, GUIComponent endNode, float endNodeMargin, SpriteBatch spriteBatch, float widthMultiplier = 1.0f) { if (endNode == null || !endNode.Visible) { return; } var endNodePos = endNode.Rect.Center.ToVector2(); @@ -1885,11 +2068,11 @@ namespace Barotrauma if ((selectedNode == null && endNode != shortcutCenterNode && GUI.IsMouseOn(endNode)) || (isSelectionHighlighted && (endNode == selectedNode || (endNode == shortcutCenterNode && shortcutNodes.Any(n => GUI.IsMouseOn(n)))))) { - GUI.DrawLine(spriteBatch, start, end, colorSource != null ? colorSource.HoverColor : Color.White, width: 4); + GUI.DrawLine(spriteBatch, start, end, colorSource?.HoverColor ?? Color.White, width: Math.Max(widthMultiplier * 4.0f, 1.0f)); } else { - GUI.DrawLine(spriteBatch, start, end, colorSource != null ? colorSource.Color : Color.White * nodeColorMultiplier, width: 2); + GUI.DrawLine(spriteBatch, start, end, colorSource?.Color ?? Color.White * nodeColorMultiplier, width: Math.Max(widthMultiplier * 2.0f, 1.0f)); } } @@ -1966,7 +2149,12 @@ namespace Barotrauma { if (commandFrame == null) { return false; } RemoveOptionNodes(); - if (targetFrame != null) { targetFrame.Visible = false; } + if (targetFrame != null) + { + targetFrame.Visible = false; + nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; + nodeConnectors.RectTransform.RepositionChildInHierarchy(1); + } // TODO: Center node could move to option node instead of being removed commandFrame.RemoveChild(centerNode); SetCenterNode(node); @@ -2126,6 +2314,8 @@ namespace Barotrauma private void CreateShortcutNodes() { + bool HasAppropriateJob(Character c, string jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); + Submarine sub = GetTargetSubmarine(); if (sub == null) { return; } @@ -2138,7 +2328,7 @@ namespace Barotrauma var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option - if ((Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && + if ((Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "operatereactor")) && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); @@ -2151,7 +2341,7 @@ namespace Barotrauma // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("captain")) && + if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "steer")) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { @@ -2161,8 +2351,8 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("securityofficer")) && - (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) + if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fightintruders")) && + Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders)) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fightintruders"), -1)); @@ -2170,25 +2360,43 @@ namespace Barotrauma // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("mechanic")) && - (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) + if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, "fixleaks")) && + Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("fixleaks"), -1)); } - // If player is not an engineer AND broken devices have been reported - // --> Create shortcut node for Repair Damaged Systems order - if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("engineer")) && - (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) + // --> Create shortcut nodes for the Repair orders + if (shortcutNodes.Count < maxShortCutNodeCount && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) { - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("repairsystems"), -1)); + // TODO: Doesn't work for player issued reports, because they don't have a target. + int repairNodes = 0; + string tag = "repairelectrical"; + if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); + repairNodes++; + } + tag = "repairmechanical"; + if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); + repairNodes++; + } + if (repairNodes == 0 && shortcutNodes.Count < maxShortCutNodeCount) + { + tag = "repairsystems"; + if (Character.Controlled == null || !HasAppropriateJob(Character.Controlled, tag)) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab(tag), -1)); + } + } } // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (shortcutNodes.Count < maxShortCutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) + if (shortcutNodes.Count < maxShortCutNodeCount && ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); @@ -2555,20 +2763,22 @@ namespace Barotrauma if (itemTargetFrame == null) { continue; } var anchor = Anchor.TopLeft; - if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) + if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f) { - anchor = Anchor.BottomRight; + if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) + { + anchor = Anchor.BottomRight; + } + else + { + anchor = Anchor.TopRight; + } } - else if (itemTargetFrame.RectTransform.RelativeOffset.X > 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) + else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f) { anchor = Anchor.BottomLeft; } - else if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f && itemTargetFrame.RectTransform.RelativeOffset.Y > 0.5f) - { - anchor = Anchor.TopRight; - } - GUIComponent optionElement; if (order.Options.Length > 1) { @@ -2631,7 +2841,6 @@ namespace Barotrauma { UserData = userData, Font = GUI.SmallFont, - ToolTip = item?.Name ?? GetOrderNameBasedOnContextuality(order), OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } @@ -2645,21 +2854,31 @@ namespace Barotrauma { optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - Sprite icon = null; - order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); - if (item.Prefab.MinimapIcon != null) - { - icon = item.Prefab.MinimapIcon; - } var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == userData.Item1.Identifier && o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; - CreateNodeIcon(Vector2.One, optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); + CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); optionNodes.Add(new Tuple(optionElement, Keys.None)); } optionElements.Add(optionElement); } - GUI.PreventElementOverlap(optionElements, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); + + Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20); + if (order.Identifier == "operateweapons") + { + Rectangle disallowedArea = targetFrame.GetChild().Rect; + Point originalSize = disallowedArea.Size; + disallowedArea.Size = disallowedArea.MultiplySize(0.9f); + disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2; + disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2; + GUI.PreventElementOverlap(optionElements, new List() { disallowedArea }, clampArea); + nodeConnectors.RectTransform.Parent = targetFrame.RectTransform; + nodeConnectors.RectTransform.SetAsFirstChild(); + } + else + { + GUI.PreventElementOverlap(optionElements, clampArea: clampArea); + } var shadow = new GUIFrame( new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), @@ -2731,6 +2950,12 @@ namespace Barotrauma private bool CreateAssignmentNodes(GUIComponent node) { + if (centerNode == null) + { + DisableCommandUI(); + return false; + } + var order = (node.UserData is Order) ? new Tuple(node.UserData as Order, null) : node.UserData as Tuple; @@ -2777,6 +3002,8 @@ namespace Barotrauma node = null; } targetFrame.Visible = false; + nodeConnectors.RectTransform.Parent = commandFrame.RectTransform; + nodeConnectors.RectTransform.RepositionChildInHierarchy(1); } if (shortcutCenterNode != null) { @@ -3182,16 +3409,7 @@ namespace Barotrauma private Character GetCharacterForQuickAssignment(Order order) { - var controllingCharacter = Character.Controlled != null; -#if !DEBUG - if (!controllingCharacter) { return null; } -#endif - if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter) && - (!controllingCharacter || operatingCharacter.CanHearCharacter(Character.Controlled))) - { - return operatingCharacter; - } - return GetCharactersSortedForOrder(order, false).FirstOrDefault(c => !controllingCharacter || c.CanHearCharacter(Character.Controlled)) ?? Character.Controlled; + return GetCharacterForQuickAssignment(order, Character.Controlled, characters); } private List GetCharactersForManualAssignment(Order order) @@ -3203,34 +3421,15 @@ namespace Barotrauma { return characters.Union(GetOrderableFriendlyNPCs()).Where(c => !c.IsDismissed).OrderBy(c => c.Info.DisplayName).ToList(); } - return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); - } - - private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) - { - return characters.Where(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)).Union(GetOrderableFriendlyNPCs()) - // 1. Prioritize those who are on the same submarine than the controlled character - .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) - // 2. Prioritize those who have been given the same maintenance or operate order as now issued - .ThenByDescending(c => c.CurrentOrders.Any(o => - o.Order != null && o.Order.Identifier == order.Identifier && - (order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate))) - // 3. Prioritize those with the appropriate job for the order - .ThenByDescending(c => order.HasAppropriateJob(c)) - // 4. Prioritize bots over player controlled characters - .ThenByDescending(c => c.IsBot) - // 5. Use the priority value of the current objective - .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) - // 6. Prioritize those with the best skill for the order - .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); + return GetCharactersSortedForOrder(order, characters, Character.Controlled, order.Identifier != "follow", extraCharacters: GetOrderableFriendlyNPCs()).ToList(); } private IEnumerable GetOrderableFriendlyNPCs() { + // TODO: change this so that we can get the data without having to rely on ui elements. return crewList.Content.Children.Where(c => c.UserData is Character character && character.TeamID == CharacterTeamType.FriendlyNPC).Select(c => (Character)c.UserData); } - #endregion #endregion @@ -3325,15 +3524,76 @@ namespace Barotrauma public void Save(XElement parentElement) { XElement element = new XElement("crew"); - foreach (CharacterInfo ci in characterInfos) { var infoElement = ci.Save(element); if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); } if (ci.HealthData != null) { infoElement.Add(ci.HealthData); } + if (ci.OrderData != null) { infoElement.Add(ci.OrderData); } if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); } } + SaveActiveOrders(element); parentElement.Add(element); } + + public static void ClientReadActiveOrders(IReadMessage inc) + { + ushort count = inc.ReadUInt16(); + if (count < 1) { return; } + var activeOrders = new List<(Order, float?)>(); + for (ushort i = 0; i < count; i++) + { + var orderMessageInfo = OrderChatMessage.ReadOrder(inc); + Character orderGiver = null; + if (inc.ReadBoolean()) + { + ushort orderGiverId = inc.ReadUInt16(); + orderGiver = orderGiverId != Entity.NullEntityID ? Entity.FindEntityByID(orderGiverId) as Character : null; + } + if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + { + DebugConsole.ThrowError("Invalid active order - order index out of bounds."); + continue; + } + Order orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; + Order order = orderMessageInfo.TargetType switch + { + Order.OrderTargetType.Entity => + new Order(orderPrefab, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: orderGiver), + Order.OrderTargetType.Position => + new Order(orderPrefab, orderMessageInfo.TargetPosition, orderGiver: orderGiver), + Order.OrderTargetType.WallSection => + new Order(orderPrefab, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: orderGiver), + _ => throw new NotImplementedException() + }; + if (order != null && order.TargetAllCharacters) + { + var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null; + activeOrders.Add((order, fadeOutTime)); + } + } + foreach (var (order, fadeOutTime) in activeOrders) + { + if (order.IsIgnoreOrder) + { + switch (order.TargetType) + { + case Order.OrderTargetType.Entity: + if (!(order.TargetEntity is IIgnorable ignorableEntity)) { break; } + ignorableEntity.OrderedToBeIgnored = order.Identifier == "ignorethis"; + break; + case Order.OrderTargetType.Position: + throw new NotImplementedException(); + case Order.OrderTargetType.WallSection: + if (!order.WallSectionIndex.HasValue) { break; } + if (!(order.TargetEntity is Structure s)) { break; } + if (!(s.GetSection(order.WallSectionIndex.Value) is IIgnorable ignorableWall)) { break; } + ignorableWall.OrderedToBeIgnored = order.Identifier == "ignorethis"; + break; + } + } + GameMain.GameSession?.CrewManager?.AddOrder(order, fadeOutTime); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 5b98cf1f0..1e9282575 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -189,7 +189,7 @@ namespace Barotrauma case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && - (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false))) + (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; @@ -287,6 +287,7 @@ namespace Barotrauma { case InteractionType.None: case InteractionType.Talk: + case InteractionType.Examine: return; case InteractionType.Upgrade when !UpgradeManager.CanUpgradeSub(): UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade"), IsSinglePlayer, npc); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index cd95768e9..e2a74ba9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -211,10 +211,7 @@ namespace Barotrauma { Character.Controlled = null; - if (prevControlled != null) - { - prevControlled.ClearInputs(); - } + prevControlled?.ClearInputs(); overlayColor = Color.LightGray; overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); @@ -326,7 +323,6 @@ namespace Barotrauma Level prevLevel = Level.Loaded; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - GUI.SetSavingIndicatorState(success); crewDead = false; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; @@ -484,6 +480,8 @@ namespace Barotrauma { IsFirstRound = false; CoroutineManager.StartCoroutine(DoLevelTransition(), "LevelTransition"); + bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + GUI.SetSavingIndicatorState(success && (Level.IsLoadedOutpost || transitionType != TransitionType.None)); } } @@ -534,7 +532,13 @@ namespace Barotrauma msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); - msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex); + + var selectedMissionIndices = map.GetSelectedMissionIndices(); + msg.Write((byte)selectedMissionIndices.Count()); + foreach (int selectedMissionIndex in selectedMissionIndices) + { + msg.Write((byte)selectedMissionIndex); + } msg.Write(PurchasedHullRepairs); msg.Write(PurchasedItemRepairs); msg.Write(PurchasedLostShuttles); @@ -569,6 +573,13 @@ namespace Barotrauma msg.Write(category.Identifier); msg.Write((byte)level); } + + msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + { + msg.Write(itemSwap.ItemToRemove.ID); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + } } //static because we may need to instantiate the campaign if it hasn't been done yet @@ -581,8 +592,15 @@ namespace Barotrauma string mapSeed = msg.ReadString(); UInt16 currentLocIndex = msg.ReadUInt16(); UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionIndex = msg.ReadByte(); - bool allowDebugTeleport = msg.ReadBoolean(); + + byte selectedMissionCount = msg.ReadByte(); + List selectedMissionIndices = new List(); + for (int i = 0; i < selectedMissionCount; i++) + { + selectedMissionIndices.Add(msg.ReadByte()); + } + + bool allowDebugTeleport = msg.ReadBoolean(); float? reputation = null; if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } @@ -657,6 +675,21 @@ namespace Barotrauma pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); } + ushort purchasedItemSwapCount = msg.ReadUInt16(); + List purchasedItemSwaps = new List(); + for (int i = 0; i < purchasedItemSwapCount; i++) + { + UInt16 itemToRemoveID = msg.ReadUInt16(); + Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; + + string itemToInstallIdentifier = msg.ReadString(); + ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + + if (itemToRemove == null) { continue; } + + purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + } + bool hasCharacterData = msg.ReadBoolean(); CharacterInfo myCharacterInfo = null; if (hasCharacterData) @@ -694,7 +727,7 @@ namespace Barotrauma campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); - campaign.Map.SelectMission(selectedMissionIndex); + campaign.Map.SelectMission(selectedMissionIndices); campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); @@ -703,6 +736,26 @@ namespace Barotrauma campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); campaign.UpgradeManager.PurchasedUpgrades.Clear(); + campaign.UpgradeManager.PurchasedUpgrades.Clear(); + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); + } + else + { + campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, force: true); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + item.PendingItemSwap = null; + } + } + foreach (var (identifier, rep) in factionReps) { Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index d3913d80f..c51a4a68d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -101,7 +101,8 @@ namespace Barotrauma case "cargo": CargoManager.LoadPurchasedItems(subElement); break; - case "pendingupgrades": + case "pendingupgrades": //backwards compatibility + case "upgrademanager": UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: true); break; case "pets": @@ -229,6 +230,7 @@ namespace Barotrauma { PetBehavior.LoadPets(petsElement); } + CrewManager.LoadActiveOrders(); GUI.DisableSavingIndicatorDelayed(); } @@ -264,10 +266,7 @@ namespace Barotrauma prevControlled.AIController.Enabled = false; } Character.Controlled = null; - if (prevControlled != null) - { - prevControlled.ClearInputs(); - } + prevControlled?.ClearInputs(); GUI.DisableHUD = true; while (GameMain.Instance.LoadingScreenOpen) @@ -303,7 +302,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } overlayTextColor = Color.Lerp(Color.Transparent, Color.White, (timer - 1.0f) / fadeInDuration); - timer = Math.Min(timer + CoroutineManager.DeltaTime, textDuration); + timer = Math.Min(timer + CoroutineManager.UnscaledDeltaTime, textDuration); yield return CoroutineStatus.Running; } var outpost = GameMain.GameSession.Level.StartOutpost; @@ -331,7 +330,7 @@ namespace Barotrauma while (timer < fadeInDuration) { overlayColor = Color.Lerp(Color.LightGray, Color.Transparent, timer / fadeInDuration); - timer += CoroutineManager.DeltaTime; + timer += CoroutineManager.UnscaledDeltaTime; yield return CoroutineStatus.Running; } overlayColor = Color.Transparent; @@ -446,9 +445,13 @@ namespace Barotrauma { Submarine.MainSub = leavingSub; GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); foreach (Submarine sub in subsToLeaveBehind) { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } @@ -480,6 +483,8 @@ namespace Barotrauma EnableRoundSummaryGameOverState(); } + CrewManager?.ClearCurrentOrders(); + //-------------------------------------- SelectSummaryScreen(roundSummary, newLevel, mirror, () => @@ -558,7 +563,7 @@ namespace Barotrauma } #if DEBUG - if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.R)) + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) { if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } @@ -735,9 +740,10 @@ namespace Barotrauma if (c.Inventory != null) { c.Info.InventoryData = new XElement("inventory"); - c.SaveInventory(c.Inventory, c.Info.InventoryData); + c.SaveInventory(); c.Inventory?.DeleteAllItems(); } + c.Info.SaveOrderData(); } petsElement = new XElement("pets"); @@ -748,7 +754,7 @@ namespace Barotrauma CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); - UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades); + UpgradeManager?.Save(modeElement); element.Add(modeElement); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 023d89045..60e4b869a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -123,17 +123,17 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_lockedDoor_2, null, false); var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("mechanic")); - captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "mechanic"); + captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "mechanic"); captain_mechanic.TeamID = CharacterTeamType.Team1; captain_mechanic.GiveJobItems(); var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); - captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "securityofficer"); + captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "securityofficer"); captain_security.TeamID = CharacterTeamType.Team1; captain_security.GiveJobItems(); var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); - captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "engineer"); + captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "engineer"); captain_engineer.TeamID = CharacterTeamType.Team1; captain_engineer.GiveJobItems(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 2dab531d3..1b10f65fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -94,19 +94,19 @@ namespace Barotrauma.Tutorials patient2.AIController.Enabled = false; var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); - var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "3"); + var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient1.TeamID = CharacterTeamType.Team1; subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); - var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "3"); + var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient2.TeamID = CharacterTeamType.Team1; subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); - var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "3"); + var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient3.TeamID = CharacterTeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 8c28a67ea..fe62c6228 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -531,7 +531,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (!mechanic.HasEquippedItem("divingsuit")); + } while (!mechanic.HasEquippedItem("divingsuit", slotType: InvSlotType.OuterClothes)); SetHighlight(mechanic_divingSuitContainer.Item, false); RemoveCompletedObjective(segments[8]); SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 6f90823d7..40ec19062 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -177,7 +177,7 @@ namespace Barotrauma.Tutorials } } - return WayPoint.GetRandom(spawnPointType, charInfo.Job, spawnSub); + return WayPoint.GetRandom(spawnPointType, charInfo.Job?.Prefab, spawnSub); } protected bool HasOrder(Character character, string identifier, string option = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 6d880f5ef..989889b19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -224,10 +224,7 @@ namespace Barotrauma.Tutorials public virtual void Update(float deltaTime) { - if (videoPlayer != null) - { - videoPlayer.Update(); - } + videoPlayer?.Update(); if (activeObjectives != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index ec74f7590..336830891 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -524,7 +524,7 @@ namespace Barotrauma private static void CheckIfDivingGearOutOfOxygen() { if (!CanDisplayHints()) { return; } - var divingGear = Character.Controlled.GetEquippedItem("diving"); + var divingGear = Character.Controlled.GetEquippedItem("diving", InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } DisplayHint("ondivinggearoutofoxygen", onUpdate: () => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index e0211d8af..e69e11bae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -218,12 +218,15 @@ namespace Barotrauma }; List missionsToDisplay = new List(selectedMissions); - if (!selectedMissions.Any() && startLocation?.SelectedMission != null) - { - if (startLocation.SelectedMission.Locations[0] == startLocation.SelectedMission.Locations[1] || - startLocation.SelectedMission.Locations.Contains(campaignMode?.Map.SelectedLocation)) + if (!selectedMissions.Any() && startLocation != null) + { + foreach (Mission mission in startLocation.SelectedMissions) { - missionsToDisplay.Add(startLocation.SelectedMission); + if (mission.Locations[0] == mission.Locations[1] || + mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) + { + missionsToDisplay.Add(mission); + } } } @@ -284,10 +287,11 @@ namespace Barotrauma new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); } - var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), missionContentHorizontal.RectTransform)) + var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), missionContentHorizontal.RectTransform)) { - RelativeSpacing = 0.05f + AbsoluteSpacing = GUI.IntScale(5) }; + missionContentHorizontal.Recalculate(); var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.Name, font: GUI.SubHeadingFont); if (displayedMission.Difficulty.HasValue) @@ -309,12 +313,13 @@ namespace Barotrauma }; } } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), + var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), missionMessage, wrap: true, parseRichText: true); - if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && displayedMission.Reward > 0) + int reward = displayedMission.GetReward(Submarine.MainSub); + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", displayedMission.Reward)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.GetMissionRewardText(), parseRichText: true); + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.GetMissionRewardText(Submarine.MainSub), parseRichText: true); } if (displayedMission != missionsToDisplay.Last()) @@ -322,6 +327,13 @@ namespace Barotrauma var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), missionList.Content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(15)) }, style: null); new GUIFrame(new RectTransform(new Vector2(0.8f, 1.0f), spacing.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "HorizontalLine"); } + + foreach (GUIComponent child in missionTextContent.Children) + { + child.RectTransform.IsFixedSize = true; + } + missionTextContent.RectTransform.MinSize = new Point(0, missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing)); + missionContentHorizontal.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Rect.Height / missionTextContent.RectTransform.RelativeSize.Y)); } if (!missionsToDisplay.Any()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs index e66fb012d..073893bb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/UpgradeManager.cs @@ -39,43 +39,5 @@ namespace Barotrauma } } } - - /// - /// Server has notified us that upgrades were reset. - /// - /// - /// - public void ClientRead(IReadMessage inc) - { - bool shouldReset = inc.ReadBoolean(); - int money = inc.ReadInt32(); - // uint length = inc.ReadUInt32(); - // - // for (int i = 0; i < length; i++) - // { - // string key = inc.ReadString(); - // byte value = inc.ReadByte(); - // Metadata.SetValue(key, value); - // } - - Campaign.Money = money; - - if (shouldReset) - { - ResetUpgrades(); - } - - // spentMoney is local, so this message box should only appear for those who have spent money on upgrades - if (spentMoney > 0) - { - GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new [] { TextManager.Get("Ok") }); - msgBox.Buttons[0].OnClicked += msgBox.Close; - } - - spentMoney = 0; - PendingUpgrades.Clear(); - PurchasedUpgrades.Clear(); - CanUpgrade = false; - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index fbeca0143..200d39720 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1077,8 +1077,8 @@ namespace Barotrauma new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); - var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) + var voiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); + var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) { SelectedColor = Color.Gold * 0.3f, UserData = InputType.Voice @@ -1089,14 +1089,16 @@ namespace Barotrauma new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); - var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) + var localVoiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); + var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) { SelectedColor = Color.Gold * 0.3f, UserData = InputType.LocalVoice }; localVoiceKeyBox.OnSelected += KeyBoxSelected; + voiceKeybindLabel.RectTransform.SizeChanged += () => { GUITextBlock.AutoScaleAndNormalize(voiceKeybindLabel, localVoiceKeybindLabel); }; + var cutoffPreventionText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("CutoffPrevention"), font: GUI.SubHeadingFont) { ToolTip = TextManager.Get("CutoffPreventionTooltip") @@ -1326,24 +1328,41 @@ namespace Barotrauma } }; - GUITickBox disableInGameHintsBox = new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), - TextManager.Get("DisableInGameHints")) + new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), TextManager.Get("DisableInGameHints")) { Selected = DisableInGameHints, ToolTip = TextManager.Get("DisableInGameHintsToolTip"), OnSelected = (tickBox) => { DisableInGameHints = tickBox.Selected; - if (!DisableInGameHints && GameMain.Config?.IgnoredHints != null) - { - // Reset the ignored hints when the hints are re-enabled (to-be-replaced by a separate button) - GameMain.Config.IgnoredHints.Clear(); - } UnsavedSettings = true; return true; } }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), + text: TextManager.Get("ResetInGameHints"), + style: "GUIButtonSmall") + { + OnClicked = (button, userData) => + { + var msgBox = new GUIMessageBox(TextManager.Get("ResetInGameHints"), + TextManager.Get("ResetInGameHintsTooltip"), + new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (button, userData) => + { + GameMain.Config.IgnoredHints.Clear(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + }; + GUITextBlock HUDScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("HUDScale"), font: GUI.SubHeadingFont, wrap: true); GUIScrollBar HUDScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), style: "GUISlider", barSize: 0.1f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 8120ab775..63235d0d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -297,11 +297,11 @@ namespace Barotrauma SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; + hideButton.Visible = false; + if (visualSlots == null) { CreateSlots(); } if (visualSlots.None()) { return; } - hideButton.Visible = false; - switch (layout) { case Layout.Default: @@ -537,9 +537,12 @@ namespace Barotrauma } List hideSubInventories = new List(); + //remove highlighted subinventory slots that can no longer be accessed highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == this && ((s.SlotIndex < 0 || s.SlotIndex >= slots.Length || slots[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); + //remove highlighted subinventory slots that refer to items no longer in this inventory + highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && s.Item.ParentInventory != this); foreach (var highlightedSubInventorySlot in highlightedSubInventorySlots) { if (highlightedSubInventorySlot.ParentInventory == this) @@ -910,7 +913,7 @@ namespace Barotrauma else if (allowEquip) //doubleclicked and no other inventory is selected { //not equipped -> attempt to equip - if (!character.HasEquippedItem(item)) + if (!character.HasEquippedItem(item) || item.GetComponents().Count() > 1) { return QuickUseAction.Equip; } @@ -1100,6 +1103,7 @@ namespace Barotrauma public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead) { return; } + if (capacity == 0) { return; } if (visualSlots == null) { CreateSlots(); } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index c9c4fcb7a..d57f1ee61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -281,10 +281,10 @@ namespace Barotrauma.Items.Components foreach (ParticleEmitter particleEmitter in particleEmitters) { float particleAngle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); - float particleRange = particleEmitter.Prefab.VelocityMax * particleEmitter.Prefab.ParticlePrefab.LifeTime; + float particleRange = particleEmitter.Prefab.Properties.VelocityMax * particleEmitter.Prefab.ParticlePrefab.LifeTime; particleEmitter.Emit( deltaTime, particleStartPos, - item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0, velocityMultiplier: dist / particleRange * 1.5f, + item.CurrentHull, particleAngle, particleEmitter.Prefab.Properties.CopyEntityAngle ? -particleAngle : 0, velocityMultiplier: dist / particleRange * 1.5f, colorMultiplier: new Color(color.R, color.G, color.B, (byte)255)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index f99a471ca..289c88ec2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -611,5 +611,6 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } + public virtual void AddTooltipInfo(ref string description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index ee39accbf..05c6b22bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -14,6 +14,11 @@ namespace Barotrauma.Items.Components private GUICustomComponent guiCustomComponent; + /// + /// Can be used to set the sprite depth individually for each contained item + /// + private float[] containedSpriteDepths; + public Sprite InventoryTopSprite { get { return inventoryTopSprite; } @@ -153,6 +158,8 @@ namespace Barotrauma.Items.Components //if a GUIFrame has been defined, draw the inventory inside it CreateGUI(); } + + containedSpriteDepths = element.GetAttributeFloatArray("containedspritedepths", new float[0]); } protected override void CreateGUI() @@ -316,6 +323,10 @@ namespace Barotrauma.Items.Components if (item.FlippedY) { origin.Y = containedItem.Sprite.SourceRect.Height - origin.Y; } float containedSpriteDepth = ContainedSpriteDepth < 0.0f ? containedItem.Sprite.Depth : ContainedSpriteDepth; + if (i < containedSpriteDepths.Length) + { + containedSpriteDepth = containedSpriteDepths[i]; + } containedSpriteDepth = itemDepth + (containedSpriteDepth - (item.Sprite?.Depth ?? item.SpriteDepth)) / 10000.0f; containedItem.Sprite.Draw( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index b3a228c25..f58aee17b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -16,44 +16,43 @@ namespace Barotrauma.Items.Components public Vector2 DrawSize { - get { return new Vector2(light.Range * 2, light.Range * 2); } + get { return new Vector2(Light.Range * 2, Light.Range * 2); } } - private LightSource light; - public LightSource Light - { - get { return light; } - } + public LightSource Light { get; } public override void OnScaleChanged() { - light.SpriteScale = Vector2.One * item.Scale; - light.Position = ParentBody != null ? ParentBody.Position : item.Position; + Light.SpriteScale = Vector2.One * item.Scale; + Light.Position = ParentBody != null ? ParentBody.Position : item.Position; } partial void SetLightSourceState(bool enabled, float brightness) { - if (light == null) { return; } - light.Enabled = enabled; - light.Color = LightColor.Multiply(brightness); + if (Light == null) { return; } + Light.Enabled = enabled; + if (enabled) + { + Light.Color = LightColor.Multiply(brightness); + } } public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) + if (Light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) { - Vector2 origin = light.LightSprite.Origin; - if ((light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } - if ((light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } - light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, itemDepth - 0.0001f); + Vector2 origin = Light.LightSprite.Origin; + if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } + if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } + Light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); } } public override void FlipX(bool relativeToSub) { - if (light?.LightSprite != null && item.Prefab.CanSpriteFlipX && item.body == null) + if (Light?.LightSprite != null && item.Prefab.CanSpriteFlipX && item.body == null) { - light.LightSpriteEffect = light.LightSpriteEffect == SpriteEffects.None ? + Light.LightSpriteEffect = Light.LightSpriteEffect == SpriteEffects.None ? SpriteEffects.FlipHorizontally : SpriteEffects.None; } } @@ -93,7 +92,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); - light.Remove(); + Light.Remove(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index ecc4b8b83..4d7ee5ea1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -178,26 +178,26 @@ namespace Barotrauma.Items.Components ToolTip = fi.TargetItem.Description }; - var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), - childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; + var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), + childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; - var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.sprite; - if (itemIcon != null) - { - new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform), - itemIcon, scaleToFit: true) - { - Color = fi.TargetItem.InventoryIconColor, - ToolTip = fi.TargetItem.Description - }; - } + var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.sprite; + if (itemIcon != null) + { + new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform), + itemIcon, scaleToFit: true) + { + Color = fi.TargetItem.InventoryIconColor, + ToolTip = fi.TargetItem.Description + }; + } - new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform), GetRecipeNameAndAmount(fi)) - { - Padding = Vector4.Zero, - AutoScaleVertical = true, - ToolTip = fi.TargetItem.Description - }; + new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform), GetRecipeNameAndAmount(fi)) + { + Padding = Vector4.Zero, + AutoScaleVertical = true, + ToolTip = fi.TargetItem.Description + }; } } @@ -367,6 +367,10 @@ namespace Barotrauma.Items.Components { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } + else if (requiredItem.MaxCondition <= 0.0f) + { + toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); + } if (!string.IsNullOrEmpty(requiredItem.ItemPrefabs.First().Description)) { toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index d98beb236..0ec56396d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -199,13 +199,9 @@ namespace Barotrauma.Items.Components } Color neutralColor = Color.DarkCyan; - if (hull.RoomName != null) + if (hull.IsWetRoom) { - if (hull.RoomName.Contains("ballast") || hull.RoomName.Contains("Ballast") || - hull.RoomName.Contains("airlock") || hull.RoomName.Contains("Airlock")) - { - neutralColor = new Color(9, 80, 159); - } + neutralColor = new Color(9, 80, 159); } if (hullData.Distort) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs index 7d9ff5175..80e5671a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/OutpostTerminal.cs @@ -33,10 +33,7 @@ namespace Barotrauma.Items.Components base.Update(deltaTime, cam); - if (selectionUI != null) - { - selectionUI.Update(); - } + selectionUI?.Update(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index e35c9a17b..3877a2ccf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -16,8 +16,8 @@ namespace Barotrauma.Items.Components private GUITickBox powerLight; private GUITickBox autoControlIndicator; - private List> pumpOutEmitters = new List>(); - private List> pumpInEmitters = new List>(); + private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpOutEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); + private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpInEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); public float CurrentBrokenVolume { @@ -35,14 +35,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "pumpoutemitter": - pumpOutEmitters.Add(new Pair( - subElement.GetAttributeVector2("position", Vector2.Zero), - new ParticleEmitter(subElement))); + pumpOutEmitters.Add((subElement.GetAttributeVector2("position", Vector2.Zero), new ParticleEmitter(subElement))); break; case "pumpinemitter": - pumpInEmitters.Add(new Pair( - subElement.GetAttributeVector2("position", Vector2.Zero), - new ParticleEmitter(subElement))); + pumpInEmitters.Add((subElement.GetAttributeVector2("position", Vector2.Zero), new ParticleEmitter(subElement))); break; } } @@ -148,21 +144,43 @@ namespace Barotrauma.Items.Components { if (FlowPercentage < 0.0f) { - foreach (Pair pumpOutEmitter in pumpOutEmitters) + foreach (var (position, emitter) in pumpOutEmitters) { - //only emit "pump out" particles when underwater - Vector2 particlePos = item.Rect.Location.ToVector2() + pumpOutEmitter.First; - if (item.CurrentHull != null && item.CurrentHull.Surface < particlePos.Y) continue; + if (item.CurrentHull != null && item.CurrentHull.Surface < item.Rect.Location.Y + position.Y) { continue; } - pumpOutEmitter.Second.Emit(deltaTime, item.WorldRect.Location.ToVector2() + pumpOutEmitter.First * item.Scale, item.CurrentHull, + //only emit "pump out" particles when underwater + Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; + float angle = 0.0f; + if (item.FlippedX) + { + relativeParticlePos.X = -relativeParticlePos.X; + angle = MathHelper.Pi; + } + if (item.FlippedY) + { + relativeParticlePos.Y = -relativeParticlePos.Y; + } + + emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle, velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, -FlowPercentage / 100.0f)); } } else if (FlowPercentage > 0.0f) { - foreach (Pair pumpInEmitter in pumpInEmitters) + foreach (var (position, emitter) in pumpInEmitters) { - pumpInEmitter.Second.Emit(deltaTime, item.WorldRect.Location.ToVector2() + pumpInEmitter.First * item.Scale, item.CurrentHull, + Vector2 relativeParticlePos = (item.WorldRect.Location.ToVector2() + position * item.Scale) - item.WorldPosition; + float angle = 0.0f; + if (item.FlippedX) + { + relativeParticlePos.X = -relativeParticlePos.X; + angle = MathHelper.Pi; + } + if (item.FlippedY) + { + relativeParticlePos.Y = -relativeParticlePos.Y; + } + emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle, velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, FlowPercentage / 100.0f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index b8d80bbb9..275ffa13a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -53,6 +53,8 @@ namespace Barotrauma.Items.Components private GUIFrame inventoryContainer; + private GUILayoutGroup paddedFrame; + private readonly Dictionary warningButtons = new Dictionary(); private static readonly string[] warningTexts = new string[] @@ -74,7 +76,7 @@ namespace Barotrauma.Items.Components tempRangeIndicator = new Sprite(element.GetChildElement("temprangeindicator")?.GetChildElement("sprite")); graphLine = new Sprite(element.GetChildElement("graphline")?.GetChildElement("sprite")); - var paddedFrame = new GUILayoutGroup(new RectTransform( + paddedFrame = new GUILayoutGroup(new RectTransform( GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, isHorizontal: true) @@ -128,27 +130,27 @@ namespace Barotrauma.Items.Components Point maxIndicatorSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)); - criticalHeatWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, + criticalHeatWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, TextManager.Get("ReactorWarningCriticalTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorHeatTip") }; - lowTemperatureWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") - { - Selected = false, - Enabled = false, - ToolTip = TextManager.Get("ReactorTempTip") - }; - criticalOutputWarning = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, + criticalOutputWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, TextManager.Get("ReactorWarningCriticalOutput"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorOutputTip") }; + lowTemperatureWarning = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, + TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + { + Selected = false, + Enabled = false, + ToolTip = TextManager.Get("ReactorTempTip") + }; List indicatorLights = new List() { criticalHeatWarning, lowTemperatureWarning, criticalOutputWarning }; indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUI.Style.TextColor)); topLeftArea.Recalculate(); @@ -334,8 +336,9 @@ namespace Barotrauma.Items.Components }; topRightArea.Recalculate(); - autoTempLight.TextBlock.Wrap = true; - indicatorLights.Add(autoTempLight); + autoTempLight.TextBlock.Padding = new Vector4(autoTempLight.TextBlock.Padding.X, 0.0f, 0.0f, 0.0f); + autoTempLight.TextBlock.Text = autoTempLight.TextBlock.Text.Replace(' ', '\n'); + autoTempLight.TextBlock.AutoScaleHorizontal = true; GUITextBlock.AutoScaleAndNormalize(indicatorLights.Select(l => l.TextBlock)); // right bottom (graph area) ----------------------- @@ -533,8 +536,7 @@ namespace Barotrauma.Items.Components warningButtons["ReactorWarningMeltdown"].Selected = meltDownTimer > MeltdownDelay * 0.5f || item.Condition == 0.0f && lightOn; warningButtons["ReactorWarningSCRAM"].Selected = temperature > 0.1f && !PowerOn; - if ((FissionRateScrollBar.Rect.Contains(PlayerInput.MousePosition) || FissionRateScrollBar.Children.Contains(GUIScrollBar.DraggingBar) || - TurbineOutputScrollBar.Rect.Contains(PlayerInput.MousePosition) || TurbineOutputScrollBar.Children.Contains(GUIScrollBar.DraggingBar)) && + if (paddedFrame.Rect.Contains(PlayerInput.MousePosition) && !PlayerInput.KeyDown(InputType.Deselect) && !PlayerInput.KeyHit(InputType.Deselect)) { Character.DisableControls = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 11c39d491..dbf89512b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -87,21 +87,6 @@ namespace Barotrauma.Items.Components //float = strength of the disruption, between 0-1 private readonly List> disruptedDirections = new List>(); - class CachedDistance - { - public readonly Vector2 TransducerWorldPos; - public readonly Vector2 WorldPos; - public readonly float Distance; - public double RecalculationTime; - - public CachedDistance(Vector2 transducerWorldPos, Vector2 worldPos, float dist) - { - TransducerWorldPos = transducerWorldPos; - WorldPos = worldPos; - Distance = dist; - } - } - private readonly Dictionary markerDistances = new Dictionary(); private readonly Color positiveColor = Color.Green; @@ -451,7 +436,9 @@ namespace Barotrauma.Items.Components connectedSubUpdateTimer = ConnectedSubUpdateInterval; } - if (sonarView.Rect.Contains(PlayerInput.MousePosition)) + Steering steering = item.GetComponent(); + if (sonarView.Rect.Contains(PlayerInput.MousePosition) && + (GUI.MouseOn == null || GUI.MouseOn == sonarView || sonarView.IsParentOf(GUI.MouseOn) || GUI.MouseOn == steering?.GuiFrame || (steering?.GuiFrame?.IsParentOf(GUI.MouseOn) ?? false))) { float scrollSpeed = PlayerInput.ScrollWheelSpeed / 1000.0f; if (Math.Abs(scrollSpeed) > 0.0001f) @@ -459,8 +446,13 @@ namespace Barotrauma.Items.Components zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f; zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); } + + if (PlayerInput.KeyHit(InputType.Run)) + { + SonarModeSwitch.OnClicked(SonarModeSwitch, null); + } } - + float distort = 1.0f - item.Condition / item.MaxCondition; for (int i = sonarBlips.Count - 1; i >= 0; i--) { @@ -623,7 +615,6 @@ namespace Barotrauma.Items.Components } } - Steering steering = item.GetComponent(); if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null) { float dockingDist = Vector2.Distance(steering.ActiveDockingSource.Item.WorldPosition, steering.DockingTarget.Item.WorldPosition); @@ -790,10 +781,7 @@ namespace Barotrauma.Items.Components DisplayRadius = (rect.Width / 2.0f) * (1.0f - displayBorderSize); displayScale = DisplayRadius / range * zoom; - if (screenBackground != null) - { - screenBackground.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); - } + screenBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X); if (useDirectionalPing) { @@ -881,10 +869,7 @@ namespace Barotrauma.Items.Components GUI.DrawString(spriteBatch, rect.Location.ToVector2(), sonarBlips.Count.ToString(), Color.White); } - if (screenOverlay != null) - { - screenOverlay.Draw(spriteBatch, center, 0.0f, rect.Width / screenOverlay.size.X); - } + screenOverlay?.Draw(spriteBatch, center, 0.0f, rect.Width / screenOverlay.size.X); if (signalStrength <= 0.5f) { @@ -947,21 +932,25 @@ namespace Barotrauma.Items.Components cave.StartPos.ToVector2(), transducerCenter, displayScale, center, DisplayRadius); } - + + int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { if (!string.IsNullOrWhiteSpace(mission.SonarLabel)) { + int i = 0; foreach (Vector2 sonarPosition in mission.SonarPositions) { DrawMarker(spriteBatch, mission.SonarLabel, mission.SonarIconIdentifier, - mission, - sonarPosition, transducerCenter, + "mission" + missionIndex + ":" + i, + sonarPosition, transducerCenter, displayScale, center, DisplayRadius * 0.95f); + i++; } } + missionIndex++; } if (AllowUsingMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) @@ -974,7 +963,7 @@ namespace Barotrauma.Items.Components var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } DrawMarker(spriteBatch, - i.Name, "mineral", i, + i.Name, "mineral", "mineralcluster" + i, c.center, transducerCenter, displayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); @@ -987,14 +976,14 @@ namespace Barotrauma.Items.Components if (connectedSubs.Contains(sub)) { continue; } if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } - if (item.Submarine != null) + if (item.Submarine != null || Character.Controlled != null) { //hide enemy team - if (sub.TeamID == CharacterTeamType.Team1 && (item.Submarine.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2)) + if (sub.TeamID == CharacterTeamType.Team1 && (item.Submarine?.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2)) { continue; } - else if (sub.TeamID == CharacterTeamType.Team2 && (item.Submarine.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1)) + else if (sub.TeamID == CharacterTeamType.Team2 && (item.Submarine?.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1)) { continue; } @@ -1101,19 +1090,17 @@ namespace Barotrauma.Items.Components if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } if (dockingPort.Item.Submarine == null) { continue; } if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } - if (!dockingPort.Item.Submarine.ShowSonarMarker && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } + // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's + if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } //don't show the docking ports of the opposing team on the sonar - if (item.Submarine != null) + if (item.Submarine != null && + item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) { - if (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team1 && (item.Submarine.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2)) - { - continue; - } - else if (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team2 && (item.Submarine.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1)) - { - continue; - } + // specifically checking for friendlyNPC seems more logical here + if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } } Vector2 offset = (dockingPort.Item.WorldPosition - transducerCenter) * scale; @@ -1629,9 +1616,7 @@ namespace Barotrauma.Items.Components { if (markerDistances.TryGetValue(targetIdentifier, out CachedDistance cachedDistance)) { - if (Timing.TotalTime > cachedDistance.RecalculationTime && - (Vector2.DistanceSquared(cachedDistance.TransducerWorldPos, transducerPosition) > 500 * 500 || - Vector2.DistanceSquared(cachedDistance.WorldPos, worldPosition) > 500 * 500)) + if (cachedDistance.ShouldUpdateDistance(transducerPosition, worldPosition)) { markerDistances.Remove(targetIdentifier); CalculateDistance(); @@ -1653,10 +1638,7 @@ namespace Barotrauma.Items.Components var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); if (!path.Unreachable) { - var cachedDistance = new CachedDistance(transducerPosition, worldPosition, path.TotalLength) - { - RecalculationTime = Timing.TotalTime + Rand.Range(1.0f, 5.0f) - }; + var cachedDistance = new CachedDistance(transducerPosition, worldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f)); markerDistances.Add(targetIdentifier, cachedDistance); dist = path.TotalLength; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index afa0d6f39..6fce23838 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -1,12 +1,18 @@ using Barotrauma.Networking; +using Barotrauma.Particles; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent { + private readonly List particleEmitters = new List(); + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { bool launch = msg.ReadBoolean(); @@ -96,5 +102,30 @@ namespace Barotrauma.Items.Components Unstick(); } } + + partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation) + { + Vector2 particlePos = item.WorldPosition; + float rotation = -item.body.Rotation; + if (item.body.Dir < 0.0f) { rotation += MathHelper.Pi; } + Tuple tracerPoints = new Tuple(startLocation, endLocation); + foreach (ParticleEmitter emitter in particleEmitters) + { + emitter.Emit(1.0f, particlePos, hullGuess: null, angle: rotation, particleRotation: rotation, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier, tracerPoints: tracerPoints); + } + } + + partial void InitProjSpecific(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "particleemitter": + particleEmitters.Add(new ParticleEmitter(subElement)); + break; + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index a57f4abfa..93af5d8f2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -73,7 +73,7 @@ namespace Barotrauma.Items.Components } particleEmitter.Emit( deltaTime, ConvertUnits.ToDisplayUnits(raystart), - item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0); + item.CurrentHull, particleAngle, particleEmitter.Prefab.Properties.CopyEntityAngle ? -particleAngle : 0); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index f72793dd5..f68e5f37d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -36,12 +36,30 @@ namespace Barotrauma.Items.Components get { if (target == null || source == null) { return Vector2.Zero; } + + Vector2 sourcePos = GetSourcePos(); + return new Vector2( - Math.Abs(target.DrawPosition.X - source.DrawPosition.X), - Math.Abs(target.DrawPosition.Y - source.DrawPosition.Y)) * 1.5f; + Math.Abs(target.DrawPosition.X - sourcePos.X), + Math.Abs(target.DrawPosition.Y - sourcePos.Y)) * 1.5f; } } + private Vector2 GetSourcePos() + { + Vector2 sourcePos = source.WorldPosition; + if (source is Item sourceItem) + { + sourcePos = sourceItem.DrawPosition; + } + else if (source is Limb sourceLimb && sourceLimb.body != null) + { + sourcePos = sourceLimb.body.DrawPosition; + } + return sourcePos; + + } + partial void InitProjSpecific(XElement element) { foreach (XElement subElement in element.Elements()) @@ -65,14 +83,18 @@ namespace Barotrauma.Items.Components { if (target == null) { return; } - Vector2 startPos = new Vector2(source.DrawPosition.X, -source.DrawPosition.Y); - var turret = source?.GetComponent(); - if (turret != null) + Vector2 startPos = GetSourcePos(); + startPos.Y = -startPos.Y; + if (source is Item sourceItem) { - startPos = new Vector2(source.WorldRect.X + turret.TransformedBarrelPos.X, -(source.WorldRect.Y - turret.TransformedBarrelPos.Y)); - if (turret.BarrelSprite != null) + var turret = sourceItem?.GetComponent(); + if (turret != null) { - startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; + startPos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, -(sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y)); + if (turret.BarrelSprite != null) + { + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; + } } } Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); @@ -80,7 +102,7 @@ namespace Barotrauma.Items.Components if (Snapped) { float snapState = 1.0f - snapTimer / SnapAnimDuration; - Vector2 diff = target.DrawPosition - source.DrawPosition; + Vector2 diff = target.DrawPosition - new Vector2(startPos.X, -startPos.Y); diff.Y = -diff.Y; int width = (int)(SpriteWidth * snapState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 963b5b9c7..1bedbf2d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components Wire equippedWire = null; bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; - if (allowRewiring && (!panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) + if (allowRewiring && (!panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen)) { //if the Character using the panel has a wire item equipped //and the wire hasn't been connected yet, draw it on the panel @@ -365,7 +365,7 @@ namespace Barotrauma.Items.Components ConnectionPanel.HighlightedWire = wire; bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; - if (allowRewiring && (!wire.Locked && !panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) + if (allowRewiring && (!wire.Locked && !panel.Locked && !panel.TemporarilyLocked || Screen.Selected == GameMain.SubEditorScreen)) { //start dragging the wire if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index ce86894a0..f83259306 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -367,9 +367,9 @@ namespace Barotrauma.Items.Components //a wire has been selected -> check if we should start dragging one of the nodes float nodeSelectDist = 10, sectionSelectDist = 5; highlightedNodeIndex = null; - if (MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList[0] is Item) + if (MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList.FirstOrDefault() is Item selectedItem) { - Wire selectedWire = ((Item)MapEntity.SelectedList[0]).GetComponent(); + Wire selectedWire = selectedItem.GetComponent(); if (selectedWire != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 67772a0a2..8b9e7cf42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -17,15 +17,6 @@ namespace Barotrauma.Items.Components TextManager.Get("CatastrophicBleeding") }; - private static readonly string[] HealthTexts = - { - TextManager.Get("NoInjuries"), - TextManager.Get("MinorInjuries"), - TextManager.Get("Injuries"), - TextManager.Get("MajorInjuries"), - TextManager.Get("CriticalInjuries") - }; - private static readonly string[] OxygenTexts = { TextManager.Get("OxygenNormal"), @@ -55,6 +46,8 @@ namespace Barotrauma.Items.Components private Character equipper; + private bool isEquippable; + public IEnumerable VisibleCharacters { get @@ -64,14 +57,28 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + isEquippable = item.GetComponent() != null; + if (!isEquippable) { IsActive = true; } + } + public override void Update(float deltaTime, Camera cam) { base.Update(deltaTime, cam); - if (equipper == null || equipper.Removed) + Entity refEntity = equipper; + if (isEquippable) { - IsActive = false; - return; + if (equipper == null || equipper.Removed) + { + IsActive = false; + return; + } + } + else + { + refEntity = item; } if (updateTimer > 0.0f) @@ -85,11 +92,11 @@ namespace Barotrauma.Items.Components { if (c == equipper || !c.Enabled || c.Removed) { continue; } - float dist = Vector2.DistanceSquared(equipper.WorldPosition, c.WorldPosition); + float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) { - Vector2 diff = c.WorldPosition - equipper.WorldPosition; - if (Submarine.CheckVisibility(equipper.SimPosition, equipper.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) + Vector2 diff = c.WorldPosition - refEntity.WorldPosition; + if (Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) { visibleCharacters.Add(c); } @@ -147,9 +154,13 @@ namespace Barotrauma.Items.Components List texts = new List(); List textColors = new List(); - texts.Add(target.Info == null ? target.DisplayName : target.Info.DisplayName); - textColors.Add(GUI.Style.TextColor); + Color nameColor = GUI.Style.TextColor; + if (Character.Controlled != null && target.TeamID != Character.Controlled.TeamID) + { + nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + } + textColors.Add(nameColor); if (target.IsDead) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 8991ef88a..581fd311e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -34,7 +34,9 @@ namespace Barotrauma.Items.Components private RoundSound startMoveSound, endMoveSound, moveSound; - private SoundChannel moveSoundChannel; + private RoundSound chargeSound; + + private SoundChannel moveSoundChannel, chargeSoundChannel; private Vector2 oldRotation = Vector2.Zero; private Vector2 crosshairPos, crosshairPointerPos; @@ -43,11 +45,16 @@ namespace Barotrauma.Items.Components private float prevAngle; private bool flashLowPower; - private bool flashNoAmmo; + private bool flashNoAmmo, flashLoaderBroken; private float flashTimer; - private float flashLength = 1; + private readonly float flashLength = 1; + + private const float MaxCircle = 360f; + private const float HalfCircle = 180f; + private const float QuarterCircle = 90f; private readonly List particleEmitters = new List(); + private readonly List particleEmitterCharges = new List(); [Editable, Serialize("0,0,0,0", true, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] public Color HudTint @@ -77,6 +84,21 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(0.0f, false, description: "The distance in which the spinning barrels rotate. Only used if spinning barrels are created.")] + public float SpinningBarrelDistance + { + get; + private set; + } + + [Serialize(false, false, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] + public bool UseFiringOffsetForMuzzleFlash + { + get; + private set; + } + + public Vector2 DrawSize { get @@ -124,9 +146,15 @@ namespace Barotrauma.Items.Components case "movesound": moveSound = Submarine.LoadRoundSound(subElement, false); break; + case "chargesound": + chargeSound = Submarine.LoadRoundSound(subElement, false); + break; case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); break; + case "particleemittercharge": + particleEmitterCharges.Add(new ParticleEmitter(subElement)); + break; } } @@ -150,7 +178,7 @@ namespace Barotrauma.Items.Components { recoilTimer = RetractionTime; PlaySound(ActionType.OnUse); - Vector2 particlePos = new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y); + Vector2 particlePos = GetRelativeFiringPosition(UseFiringOffsetForMuzzleFlash); foreach (ParticleEmitter emitter in particleEmitters) { emitter.Emit(1.0f, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation); @@ -215,12 +243,55 @@ namespace Barotrauma.Items.Components } } + float chargeRatio = currentChargeTime / MaxChargeTime; + currentBarrelSpin = (currentBarrelSpin + MaxCircle * chargeRatio * deltaTime * 3f) % MaxCircle; + + switch (currentChargingState) + { + case ChargingState.WindingUp: + Vector2 particlePos = GetRelativeFiringPosition(); + float sizeMultiplier = Math.Clamp(chargeRatio, 0.1f, 1f); + foreach (ParticleEmitter emitter in particleEmitterCharges) + { + // color is currently not connected to ammo type, should be updated when ammo is changed + emitter.Emit(deltaTime, particlePos, hullGuess: null, angle: -rotation, particleRotation: rotation, sizeMultiplier: sizeMultiplier, colorMultiplier: emitter.Prefab.Properties.ColorMultiplier); + } + + if (chargeSoundChannel == null || !chargeSoundChannel.IsPlaying) + { + if (chargeSound != null) + { + chargeSoundChannel = SoundPlayer.PlaySound(chargeSound.Sound, item.WorldPosition, chargeSound.Volume, chargeSound.Range, ignoreMuffling: chargeSound.IgnoreMuffling); + if (chargeSoundChannel != null) chargeSoundChannel.Looping = true; + } + } + else if (chargeSoundChannel != null) + { + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); + } + break; + default: + if (chargeSoundChannel != null) + { + if (chargeSoundChannel.IsPlaying) + { + chargeSoundChannel.FadeOutAndDispose(); + chargeSoundChannel.Looping = false; + } + else + { + chargeSoundChannel = null; + } + } + break; + } + if (moveSoundChannel != null && moveSoundChannel.IsPlaying) { moveSoundChannel.Gain = MathHelper.Clamp(Math.Abs(angularVelocity), 0.5f, 1.0f); } - if (flashLowPower || flashNoAmmo) + if (flashLowPower || flashNoAmmo || flashLoaderBroken) { flashTimer += deltaTime; if (flashTimer >= flashLength) @@ -228,6 +299,7 @@ namespace Barotrauma.Items.Components flashTimer = 0; flashLowPower = false; flashNoAmmo = false; + flashLoaderBroken = false; } } } @@ -289,6 +361,42 @@ namespace Barotrauma.Items.Components rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); + float chargeRatio = currentChargeTime / MaxChargeTime; + + foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) + { + chargeSprite?.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + item.SpriteColor, + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); + } + + int spinningBarrelCount = spinningBarrelSprites.Count; + + for (int i = 0; i < spinningBarrelCount; i++) + { + // this block is messy since I was debugging it with a bunch of values, should be cleaned up / optimized if prototype is accepted + Sprite spinningBarrel = spinningBarrelSprites[i]; + float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; + + float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); + + float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; + float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; + Color newColorModifier = Color.Lerp(Color.Black, Color.Gray, colorOffset); + + float barrelHalfCirclePosition = Math.Abs(barrelCirclePosition - HalfCircle); + float barrelPositionModifier = MathUtils.SmoothStep(barrelHalfCirclePosition / HalfCircle); + float newPositionOffset = barrelPositionModifier * SpinningBarrelDistance; + + spinningBarrel.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(newPositionOffset, 0f) * item.Scale, rotation + MathHelper.PiOver2), + Color.Lerp(item.SpriteColor, newColorModifier, 0.8f), + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, newDepth); + } + if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } const float widgetRadius = 60.0f; @@ -552,14 +660,20 @@ namespace Barotrauma.Items.Components new VisualSlot(new Rectangle(invSlotPos + new Point((i % slotsPerRow) * (slotSize.X + spacing.X), (int)Math.Floor(i / (float)slotsPerRow) * (slotSize.Y + spacing.Y)), slotSize)), availableAmmo[i], -1, true); } + Rectangle rect = new Rectangle(invSlotPos.X, invSlotPos.Y, totalWidth, slotSize.Y); + float inflate = MathHelper.Lerp(3, 8, (float)Math.Abs(Math.Sin(flashTimer * 5))); + rect.Inflate(inflate, inflate); + Color color = GUI.Style.Red * Math.Max(0.5f, (float)Math.Sin(flashTimer * 12)); if (flashNoAmmo) { - Rectangle rect = new Rectangle(invSlotPos.X, invSlotPos.Y, totalWidth, slotSize.Y); - float inflate = MathHelper.Lerp(3, 8, (float)Math.Abs(1 * Math.Sin(flashTimer * 5))); - rect.Inflate(inflate, inflate); - Color color = GUI.Style.Red * MathHelper.Max(0.5f, (float)Math.Sin(flashTimer * 12)); GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); } + else if (flashLoaderBroken) + { + GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); + GUI.BrokenIcon.Draw(spriteBatch, rect.Center.ToVector2(), color, scale: rect.Height / GUI.BrokenIcon.size.Y); + GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("turretloaderbroken"), new Rectangle(invSlotPos.X + totalWidth + GUI.IntScale(10), invSlotPos.Y + slotSize.Y / 2 - GUI.IntScale(9), 0, 0)); + } } float zoom = cam == null ? 1.0f : (float)Math.Sqrt(cam.Zoom); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs new file mode 100644 index 000000000..9692249c8 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Wearable + { + private void GetDamageModifierText(ref string description, float damageMultiplier, string afflictionIdentifier) + { + int roundedValue = (int)Math.Round((1 - damageMultiplier) * 100); + if (roundedValue == 0) { return; } + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? afflictionIdentifier}"; + } + + public override void AddTooltipInfo(ref string description) + { + if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f)) || SkillModifiers.Any()) + { + description += "\n"; + } + + if (damageModifiers.Any()) + { + foreach (DamageModifier damageModifier in damageModifiers) + { + if (MathUtils.NearlyEqual(damageModifier.DamageMultiplier, 1f)) + { + continue; + } + + foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) + { + GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + } + foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionTypes) + { + GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + } + } + } + if (SkillModifiers.Any()) + { + foreach (var skillModifier in SkillModifiers) + { + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + int roundedValue = (int)Math.Round(skillModifier.Value); + if (roundedValue == 0) { continue; } + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}‖color:end‖ {TextManager.Get("SkillName." + skillModifier.Key, true) ?? skillModifier.Key}"; + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index a328939a1..6c0975078 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -296,6 +296,12 @@ namespace Barotrauma } } } + + foreach (ItemComponent component in item.Components) + { + component.AddTooltipInfo(ref description); + } + if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null) { foreach (string itemName in item.OwnInventory.AllItems.Select(it => it.Name).Distinct()) @@ -320,6 +326,11 @@ namespace Barotrauma toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } if (!string.IsNullOrEmpty(description)) { toolTip += '\n' + description; } + if (item.prefab.ContentPackage != GameMain.VanillaContent && item.prefab.ContentPackage != null) + { + colorStr = XMLExtensions.ColorToString(Color.MediumPurple); + toolTip += $"\n‖color:{colorStr}‖{item.prefab.ContentPackage.Name}‖color:end‖"; + } } if (itemsInSlot.Count() > 1) { @@ -945,6 +956,15 @@ namespace Barotrauma { return CursorState.Hand; } + var container = item?.GetComponent(); + if (container == null) { continue; } + if (container.Inventory.visualSlots != null) + { + if (container.Inventory.visualSlots.Any(slot => slot.IsHighlighted)) + { + return CursorState.Hand; + } + } } } @@ -1120,9 +1140,10 @@ namespace Barotrauma if (selectedSlot == null) { if (DraggingItemToWorld && - Character.Controlled.FocusedItem?.OwnInventory != null && - (Character.Controlled.FocusedItem.GetComponent()?.HasRequiredItems(Character.Controlled, addMessage: false) ?? false) && - Character.Controlled.FocusedItem.OwnInventory.CanBePut(DraggingItems.FirstOrDefault())) + Character.Controlled.FocusedItem is { OwnInventory: { } inventory } item && item.GetComponent() is { } container && + container.HasRequiredItems(Character.Controlled, addMessage: false) && + container.AllowDragAndDrop && + inventory.CanBePut(DraggingItems.FirstOrDefault())) { bool anySuccess = false; foreach (Item it in DraggingItems) @@ -1575,7 +1596,7 @@ namespace Barotrauma } sprite.Draw(spriteBatch, itemPos, spriteColor, rotation, scale); - if (!item.AllowStealing && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) + if ((!item.AllowStealing || (inventory != null && inventory.slots[slotIndex].Items.Any(it => !it.AllowStealing))) && CharacterInventory.LimbSlotIcons.ContainsKey(InvSlotType.LeftHand)) { var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; Vector2 iconSize = new Vector2(25 * GUI.Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 9735fabb0..72dd7def7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -22,6 +22,19 @@ namespace Barotrauma private readonly List activeHUDs = new List(); + public GUIComponentStyle IconStyle { get; private set; } = null; + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + { + if (interactionType == CampaignMode.InteractionType.None) + { + IconStyle = null; + } + else + { + IconStyle = GUI.Style.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); + } + } + public IEnumerable ActiveHUDs => activeHUDs; public float LastImpactSoundTime; @@ -252,7 +265,8 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange : GetSpriteColor(); + Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); + //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; @@ -305,24 +319,27 @@ namespace Barotrauma if (prefab.ResizeHorizontal || prefab.ResizeVertical) { Vector2 size = new Vector2(rect.Width, rect.Height); - activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, - size, color: color, - textureScale: Vector2.One * Scale, - depth: depth); - fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, - textureScale: Vector2.One * Scale, - depth: depth - 0.000001f); - foreach (var decorativeSprite in Prefab.DecorativeSprites) + if (color.A > 0) { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; - if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } - if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.DrawTiled(spriteBatch, - new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), + activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); + depth: depth); + fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, + textureScale: Vector2.One * Scale, + depth: depth - 0.000001f); + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; + if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } + decorativeSprite.Sprite.DrawTiled(spriteBatch, + new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), + size, color: color, + textureScale: Vector2.One * Scale, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); + } } } else @@ -336,8 +353,11 @@ namespace Barotrauma { origin.Y = activeSprite.SourceRect.Height - origin.Y; } - activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); - fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, depth - 0.000001f); + if (color.A > 0) + { + activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); + fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, depth - 0.000001f); + } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, rotationRad, Scale, activeSprite.effects, depth - 0.001f); @@ -365,7 +385,7 @@ namespace Barotrauma float depthStep = 0.000001f; if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) { - Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand); + Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightArm); if (holdLimb?.ActiveSprite != null) { depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() + depthStep * 2; @@ -377,7 +397,7 @@ namespace Barotrauma } else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this) { - Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand); + Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftArm); if (holdLimb?.ActiveSprite != null) { depth = holdLimb.ActiveSprite.Depth + holdable.Picker.AnimController.GetDepthOffset() - depthStep * 2; @@ -442,10 +462,7 @@ namespace Barotrauma if (GameMain.DebugDraw) { - if (body != null) - { - body.DebugDraw(spriteBatch, Color.White); - } + body?.DebugDraw(spriteBatch, Color.White); } if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space)) @@ -463,7 +480,10 @@ namespace Barotrauma if (IsSelected || IsHighlighted) { - GUI.DrawRectangle(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)), new Vector2(rect.Width, rect.Height), + Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); + Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); + drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); + GUI.DrawRectangle(spriteBatch, drawPos, drawSize, Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); foreach (Rectangle t in Prefab.Triggers) @@ -675,7 +695,11 @@ namespace Barotrauma ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => { - FlipX(relativeToSub: false); + foreach (MapEntity me in SelectedList) + { + me.FlipX(relativeToSub: false); + } + if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } return true; } }; @@ -684,7 +708,11 @@ namespace Barotrauma ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => { - FlipY(relativeToSub: false); + foreach (MapEntity me in SelectedList) + { + me.FlipY(relativeToSub: false); + } + if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } return true; } }; @@ -694,7 +722,7 @@ namespace Barotrauma reloadTextureButton.OnClicked += (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(); + Sprite.ReloadTexture(updateAllSprites: true); return true; }; } @@ -702,7 +730,12 @@ namespace Barotrauma { OnClicked = (button, data) => { - Reset(); + foreach (MapEntity me in SelectedList) + { + (me as Item)?.Reset(); + (me as Structure)?.Reset(); + } + if (!SelectedList.Contains(this)) { Reset(); } CreateEditingHUD(); return true; } @@ -734,7 +767,11 @@ namespace Barotrauma if (inGame) { if (!ic.AllowInGameEditing) { continue; } - if (SerializableProperty.GetProperties(ic).Count == 0) { continue; } + if (SerializableProperty.GetProperties(ic).Count == 0 && + !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) + { + continue; + } } else { @@ -1093,29 +1130,39 @@ namespace Barotrauma nameText += $" ({idName})"; } } - texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); - foreach (ItemComponent ic in components) + bool noComponentText = true; + + if (CampaignInteractionType != CampaignMode.InteractionType.None) { - if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } - if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } - if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } - - Color color = Color.Gray; - if (ic.HasRequiredItems(character, false)) - { - if (ic is Repairable) - { - if (!IsFullCondition) { color = Color.Cyan; } - } - else - { - color = Color.Cyan; - } - } - texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameMain.Config.KeyBindText(InputType.Use)), Color.Cyan, false, false)); } - if ((PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) && CrewManager.DoesItemHaveContextualOrders(this)) + else + { + foreach (ItemComponent ic in components) + { + if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } + if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } + if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } + + Color color = Color.Gray; + if (ic.HasRequiredItems(character, false)) + { + if (ic is Repairable) + { + if (!IsFullCondition) { color = Color.Cyan; } + } + else + { + color = Color.Cyan; + } + } + texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); + noComponentText = false; + } + } + if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) { texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false, false)); } @@ -1213,6 +1260,9 @@ namespace Barotrauma } SetActiveSprite(); break; + case NetEntityEvent.Type.AssignCampaignInteraction: + CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); + break; case NetEntityEvent.Type.ApplyStatusEffect: { ActionType actionType = (ActionType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1); @@ -1327,6 +1377,11 @@ namespace Barotrauma isActive = true; + if (positionBuffer.Count > 0) + { + transformDirty = true; + } + body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity); body.LinearVelocity = newVelocity; body.AngularVelocity = newAngularVelocity; @@ -1534,11 +1589,20 @@ namespace Barotrauma } } - var item = new Item(itemPrefab, pos, sub, id: itemId) + Item item = null; + try { - SpawnedInOutpost = spawnedInOutpost, - AllowStealing = allowStealing - }; + item = new Item(itemPrefab, pos, sub, id: itemId) + { + SpawnedInOutpost = spawnedInOutpost, + AllowStealing = allowStealing + }; + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn item {itemPrefab.Name}", e); + throw; + } if (item.body != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 2e9d73a1c..5854fc17b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -63,9 +63,12 @@ namespace Barotrauma public Dictionary> DecorativeSpriteGroups = new Dictionary>(); public Sprite InventoryIcon; public Sprite MinimapIcon; + public Sprite UpgradePreviewSprite; public Sprite InfectedSprite; public Sprite DamagedInfectedSprite; + public float UpgradePreviewScale = 1.0f; + //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", false)] public Color InventoryIconColor diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index e899eb482..308010cac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -71,15 +71,15 @@ namespace Barotrauma if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, - Rand.Vector(Rand.Range(500.0f, 800.0f)), 0.0f, hull); + Rand.Vector(Rand.Range(1200.0f, 2400.0f)), 0.0f, hull); } } if (flash) { - float displayRange = flashRange.HasValue ? flashRange.Value : Attack.Range; + float displayRange = flashRange ?? Attack.Range; if (displayRange < 0.1f) { return; } - var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); + var light = new LightSource(worldPosition, displayRange, flashColor, null); CoroutineManager.StartCoroutine(DimLight(light)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 2d8875d30..a89cb1d2f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -169,40 +169,43 @@ namespace Barotrauma } } - if (EditWater) + if (!IdFreed) { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) + if (EditWater) { - if (PlayerInput.PrimaryMouseButtonHeld()) + Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); + if (Submarine.RectContains(WorldRect, position)) { - WaterVolume += 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; + if (PlayerInput.PrimaryMouseButtonHeld()) + { + WaterVolume += 1500.0f; + networkUpdatePending = true; + serverUpdateDelay = 0.5f; + } + else if (PlayerInput.SecondaryMouseButtonHeld()) + { + WaterVolume -= 1500.0f; + networkUpdatePending = true; + serverUpdateDelay = 0.5f; + } } - else if (PlayerInput.SecondaryMouseButtonHeld()) + } + else if (EditFire) + { + Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); + if (Submarine.RectContains(WorldRect, position)) { - WaterVolume -= 1500.0f; - networkUpdatePending = true; - serverUpdateDelay = 0.5f; + if (PlayerInput.PrimaryMouseButtonClicked()) + { + new FireSource(position, this, isNetworkMessage: true); + networkUpdatePending = true; + serverUpdateDelay = 0.5f; + } } } } - else if (EditFire) - { - Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - if (Submarine.RectContains(WorldRect, position)) - { - if (PlayerInput.PrimaryMouseButtonClicked()) - { - new FireSource(position, this, isNetworkMessage: true); - networkUpdatePending = true; - serverUpdateDelay = 0.5f; - } - } - } - - if (waterVolume < 1.0f) return; + + if (waterVolume < 1.0f) { return; } for (int i = 1; i < waveY.Length - 1; i++) { float maxDelta = Math.Max(Math.Abs(rightDelta[i]), Math.Abs(leftDelta[i])); @@ -629,9 +632,9 @@ namespace Barotrauma PowerConsumptionTimer = message.ReadSingle() }; } - else if (BallastFlora != null) + else { - BallastFlora.ClientRead(message, header); + BallastFlora?.ClientRead(message, header); } return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index 743c367f5..7396a16b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -210,7 +210,7 @@ namespace Barotrauma if (ParticleEmitterTriggers[i] != null && !ParticleEmitterTriggers[i].IsTriggered) { continue; } Vector2 emitterPos = LocalToWorld(Prefab.EmitterPositions[i]); ParticleEmitters[i].Emit(deltaTime, emitterPos, hullGuess: null, - angle: ParticleEmitters[i].Prefab.CopyEntityAngle ? -CurrentRotation + MathHelper.PiOver2 : 0.0f); + angle: ParticleEmitters[i].Prefab.Properties.CopyEntityAngle ? -CurrentRotation + MathHelper.Pi : 0.0f); } } @@ -293,6 +293,12 @@ namespace Barotrauma public void ClientRead(IReadMessage msg) { if (Triggers == null) { return; } + + if (Prefab.TakeLevelWallDamage) + { + float newHealth = msg.ReadRangedSingle(0.0f, Prefab.Health, 8); + AddDamage(Health - newHealth, 1.0f, null, isNetworkEvent: true); + } for (int i = 0; i < Triggers.Count; i++) { if (!Triggers[i].UseNetworkSyncing) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index a25d03711..f4ecea93d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma const int MaxVisibleObjects = 500; private Rectangle currentGridIndices; + + public bool ForceRefreshVisibleObjects; partial void UpdateProjSpecific(float deltaTime) { @@ -60,6 +62,8 @@ namespace Barotrauma if (objectGrid[x, y] == null) { continue; } foreach (LevelObject obj in objectGrid[x, y]) { + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } + if (zoom < 0.05f) { //hide if the sprite is very small when zoomed this far out @@ -154,9 +158,10 @@ namespace Barotrauma indices.Height = Math.Min(indices.Height, objectGrid.GetLength(1) - 1); float z = 0.0f; - if (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime) + if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime)) { RefreshVisibleObjects(indices, cam.Zoom); + ForceRefreshVisibleObjects = false; if (cam.Zoom < 0.1f) { //when zoomed very far out, refresh a little less often diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 150a4bfc4..bb8e93052 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -361,6 +361,7 @@ namespace Barotrauma.Lights void DrawHalo(Character character) { + if (character == null || character.Removed) { return; } Vector2 haloDrawPos = character.DrawPosition; haloDrawPos.Y = -haloDrawPos.Y; @@ -404,7 +405,7 @@ namespace Barotrauma.Lights } foreach (Item item in Item.ItemList) { - if (item.IsHighlighted && !highlightedEntities.Contains(item)) + if ((item.IsHighlighted || item.IconStyle != null) && !highlightedEntities.Contains(item)) { highlightedEntities.Add(item); } @@ -425,7 +426,14 @@ namespace Barotrauma.Lights { if (highlighted is Item item) { - item.Draw(spriteBatch, false, true); + if (item.IconStyle != null && (item != Character.Controlled.FocusedItem || Character.Controlled.FocusedItem == null)) + { + //wait until next pass + } + else + { + item.Draw(spriteBatch, false, true); + } } else if (highlighted is Character character) { @@ -434,6 +442,22 @@ namespace Barotrauma.Lights } spriteBatch.End(); + //draw items with iconstyles in the style's color + spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive, samplerState: SamplerState.LinearWrap, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); + foreach (Entity highlighted in highlightedEntities) + { + if (highlighted is Item item) + { + if (item.IconStyle != null && (item != Character.Controlled.FocusedItem || Character.Controlled.FocusedItem == null)) + { + SolidColorEffect.Parameters["color"].SetValue(item.IconStyle.Color.ToVector4()); + SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + item.Draw(spriteBatch, false, true); + } + } + } + spriteBatch.End(); + //draw characters in black with a bit of blur, leaving the white edges visible float phase = (float)(Math.Sin(Timing.TotalTime * 3.0f) + 1.0f) / 2.0f; //phase oscillates between 0 and 1 Vector4 overlayColor = Color.Black.ToVector4() * MathHelper.Lerp(0.5f, 0.9f, phase); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 28a0f056c..7a29743ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -607,15 +607,18 @@ namespace Barotrauma tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), location.LastTypeChangeMessage); } } - if (location != CurrentLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location)) && generationParams.MissionIcon != null) + if (location != CurrentLocation && generationParams.MissionIcon != null) { - 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)) + if ((CurrentLocation == currentDisplayLocation && CurrentLocation.AvailableMissions.Any(m => m.Locations.Contains(location))) || location.AvailableMissions.Any(m => m.Prefab.Type == MissionType.GoTo)) { - var availableMissions = CurrentLocation.AvailableMissions.Where(m => m.Locations.Contains(location)); - tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); + 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(); + tooltip = (new Rectangle(missionIconPos.ToPoint(), new Point(30)), TextManager.Get("mission") + '\n'+ string.Join('\n', availableMissions.Select(m => "- " + m.Name))); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 03649abd1..2627bec37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -29,20 +29,14 @@ namespace Barotrauma public static bool SelectionChanged; //which entities have been selected for editing - private static List selectedList = new List(); - public static List SelectedList - { - get - { - return selectedList; - } - } - private static List copiedList = new List(); + public static HashSet SelectedList { get; private set; } = new HashSet(); + + public static List CopiedList = new List(); private static List highlightedList = new List(); // Test feature. Not yet saved. - public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); + public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); private static float highlightTimer; @@ -78,25 +72,13 @@ namespace Barotrauma } } - public virtual bool SelectableInEditor - { - get { return true; } - } + public virtual bool SelectableInEditor => true; - public static bool SelectedAny - { - get { return selectedList.Count > 0; } - } + public static bool SelectedAny => SelectedList.Count > 0; - public static IEnumerable CopiedList - { - get { return copiedList; } - } + public bool IsSelected => SelectedList.Contains(this); - public bool IsSelected - { - get { return selectedList.Contains(this); } - } + public bool IsIncludedInSelection { get; set; } public virtual bool IsVisible(Rectangle worldView) { @@ -129,7 +111,10 @@ namespace Barotrauma { if (resizing) { - if (selectedList.Count == 0) resizing = false; + if (!SelectedAny) + { + resizing = false; + } return; } @@ -157,19 +142,19 @@ namespace Barotrauma if (MapEntityPrefab.Selected != null) { selectionPos = Vector2.Zero; - selectedList.Clear(); + SelectedList.Clear(); return; } if (GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(Keys.Delete)) { - if (selectedList.Any()) + if (SelectedAny) { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(selectedList, true)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); } - selectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); - selectedList.Clear(); + SelectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); + SelectedList.Clear(); } if (PlayerInput.IsCtrlDown()) @@ -178,7 +163,7 @@ namespace Barotrauma if (PlayerInput.KeyHit(Keys.D)) { bool terminate = false; - foreach (MapEntity entity in selectedList) + foreach (MapEntity entity in SelectedList) { if (entity is Item item && item.GetComponent() is { } planter) { @@ -201,11 +186,11 @@ namespace Barotrauma #endif if (PlayerInput.KeyHit(Keys.C)) { - Copy(selectedList); + Copy(SelectedList.ToList()); } else if (PlayerInput.KeyHit(Keys.X)) { - Cut(selectedList); + Cut(SelectedList.ToList()); } else if (PlayerInput.KeyHit(Keys.V)) { @@ -213,21 +198,21 @@ namespace Barotrauma } else if (PlayerInput.KeyHit(Keys.G)) { - if (selectedList.Any()) + if (SelectedList.Any()) { - if (SelectionGroups.ContainsKey(selectedList.Last())) + if (SelectionGroups.ContainsKey(SelectedList.Last())) { // Ungroup all selected - selectedList.ForEach(e => SelectionGroups.Remove(e)); + SelectedList.ForEach(e => SelectionGroups.Remove(e)); } else { - foreach (var entity in selectedList) + foreach (var entity in SelectedList) { // Remove the old group, if any SelectionGroups.Remove(entity); // Create a group that can be accessed with any member - SelectionGroups.Add(entity, selectedList); + SelectionGroups.Add(entity, SelectedList); } } } @@ -277,7 +262,7 @@ namespace Barotrauma Vector2 nudge = GetNudgeAmount(); if (nudge != Vector2.Zero) { - foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudge); } + foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(nudge); } } } else @@ -290,7 +275,7 @@ namespace Barotrauma //started moving selected entities if (startMovingPos != Vector2.Zero) { - Item targetContainer = GetPotentialContainer(position, selectedList); + Item targetContainer = GetPotentialContainer(position, SelectedList); if (targetContainer != null) { targetContainer.IsHighlighted = true; } @@ -313,16 +298,16 @@ namespace Barotrauma //clone if (PlayerInput.IsCtrlDown()) { - var clones = Clone(selectedList).Where(c => c != null).ToList(); - selectedList = clones; - selectedList.ForEach(c => c.Move(moveAmount)); - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); + HashSet clones = Clone(SelectedList.ToList()).Where(c => c != null).ToHashSet(); + SelectedList = clones; + SelectedList.ForEach(c => c.Move(moveAmount)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(clones), false)); } else // move { - var oldRects = selectedList.Select(e => e.Rect).ToList(); + var oldRects = SelectedList.Select(e => e.Rect).ToList(); List deposited = new List(); - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { e.Move(moveAmount); @@ -340,14 +325,14 @@ namespace Barotrauma } } - SubEditorScreen.StoreCommand(new TransformCommand(new List(selectedList),selectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); + SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList),SelectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); if (deposited.Any() && deposited.Any(entity => entity is Item)) { var depositedItems = deposited.Where(entity => entity is Item).Cast().ToList(); SubEditorScreen.StoreCommand(new InventoryPlaceCommand(targetContainer.OwnInventory, depositedItems, false)); } - deposited.ForEach(entity => { selectedList.Remove(entity); }); + deposited.ForEach(entity => { SelectedList.Remove(entity); }); } } startMovingPos = Vector2.Zero; @@ -360,7 +345,12 @@ namespace Barotrauma selectionSize.X = position.X - selectionPos.X; selectionSize.Y = selectionPos.Y - position.Y; - List newSelection = new List();// FindSelectedEntities(selectionPos, selectionSize); + foreach (MapEntity entity in mapEntityList) + { + entity.IsIncludedInSelection = false; + } + + HashSet newSelection = new HashSet();// FindSelectedEntities(selectionPos, selectionSize); if (Math.Abs(selectionSize.X) > Submarine.GridSize.X || Math.Abs(selectionSize.Y) > Submarine.GridSize.Y) { newSelection = FindSelectedEntities(selectionPos, selectionSize); @@ -369,13 +359,22 @@ namespace Barotrauma { if (highLightedEntity != null) { - if (SelectionGroups.TryGetValue(highLightedEntity, out List group)) + if (SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)) { - newSelection.AddRange(group); + foreach (MapEntity entity in group.Where(e => !newSelection.Contains(e))) + { + newSelection.Add(entity); + } + + foreach (MapEntity entity in group) + { + entity.IsIncludedInSelection = true; + } } else { newSelection.Add(highLightedEntity); + highLightedEntity.IsIncludedInSelection = true; } } } @@ -386,7 +385,7 @@ namespace Barotrauma { foreach (MapEntity e in newSelection) { - if (selectedList.Contains(e)) + if (SelectedList.Contains(e)) { RemoveSelection(e); } @@ -398,7 +397,7 @@ namespace Barotrauma } else { - selectedList = new List(newSelection); + SelectedList = new HashSet(newSelection); //selectedList.Clear(); //newSelection.ForEach(e => AddSelection(e)); foreach (var entity in newSelection) @@ -407,23 +406,23 @@ namespace Barotrauma onGapFound: (door, gap) => { door.RefreshLinkedGap(); - if (!selectedList.Contains(gap)) + if (!SelectedList.Contains(gap)) { - selectedList.Add(gap); + SelectedList.Add(gap); } }, onDoorFound: (door, gap) => { - if (!selectedList.Contains(door.Item)) + if (!SelectedList.Contains(door.Item)) { - selectedList.Add(door.Item); + SelectedList.Add(door.Item); } }); } } //select wire if both items it's connected to are selected - var selectedItems = selectedList.Where(e => e is Item).Cast().ToList(); + var selectedItems = SelectedList.Where(e => e is Item).Cast().ToList(); foreach (Item item in selectedItems) { if (item.Connections == null) continue; @@ -431,11 +430,11 @@ namespace Barotrauma { foreach (Wire w in c.Wires) { - if (w == null || selectedList.Contains(w.Item)) continue; + if (w == null || SelectedList.Contains(w.Item)) continue; - if (w.OtherConnection(c) != null && selectedList.Contains(w.OtherConnection(c).Item)) + if (w.OtherConnection(c) != null && SelectedList.Contains(w.OtherConnection(c).Item)) { - selectedList.Add(w.Item); + SelectedList.Add(w.Item); } } } @@ -443,6 +442,10 @@ namespace Barotrauma selectionPos = Vector2.Zero; selectionSize = Vector2.Zero; + foreach (MapEntity entity in mapEntityList) + { + entity.IsIncludedInSelection = false; + } } } //default, not doing anything specific yet @@ -455,7 +458,7 @@ namespace Barotrauma (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn)))) { //if clicking a selected entity, start moving it - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { if (e.IsMouseOn(position)) startMovingPos = position; } @@ -503,7 +506,7 @@ namespace Barotrauma return ReplacedBy?.GetReplacementOrThis() ?? this; } - public static Item GetPotentialContainer(Vector2 position, List entities = null) + public static Item GetPotentialContainer(Vector2 position, HashSet entities = null) { Item targetContainer = null; bool isShiftDown = PlayerInput.IsShiftDown(); @@ -638,7 +641,7 @@ namespace Barotrauma if (PlayerInput.IsCtrlDown() && !wiringMode) { - if (selectedList.Contains(entity)) + if (SelectedList.Contains(entity)) { RemoveSelection(entity); } @@ -657,56 +660,60 @@ namespace Barotrauma public static void AddSelection(MapEntity entity) { - if (selectedList.Contains(entity)) { return; } - selectedList.Add(entity); + if (SelectedList.Contains(entity)) { return; } + SelectedList.Add(entity); HandleDoorGapLinks(entity, onGapFound: (door, gap) => { door.RefreshLinkedGap(); - if (!selectedList.Contains(gap)) + if (!SelectedList.Contains(gap)) { - selectedList.Add(gap); + SelectedList.Add(gap); } }, onDoorFound: (door, gap) => { - if (!selectedList.Contains(door.Item)) + if (!SelectedList.Contains(door.Item)) { - selectedList.Add(door.Item); + SelectedList.Add(door.Item); } }); } private static void HandleDoorGapLinks(MapEntity entity, Action onGapFound, Action onDoorFound) { - if (entity is Item i) + switch (entity) { - var door = i.GetComponent(); - if (door != null) + case Item i: { - var gap = door.LinkedGap; + var door = i.GetComponent(); + var gap = door?.LinkedGap; if (gap != null) { onGapFound(door, gap); } + + break; } - } - else if (entity is Gap gap) - { - var door = gap.ConnectedDoor; - if (door != null) + case Gap gap: { - onDoorFound(door, gap); + var door = gap.ConnectedDoor; + if (door != null) + { + onDoorFound(door, gap); + } + + break; } } } public static void RemoveSelection(MapEntity entity) { - selectedList.Remove(entity); + SelectedList.Remove(entity); HandleDoorGapLinks(entity, - onGapFound: (door, gap) => selectedList.Remove(gap), - onDoorFound: (door, gap) => selectedList.Remove(door.Item)); + onGapFound: (door, gap) => SelectedList.Remove(gap), + onDoorFound: (door, gap) => SelectedList.Remove(door.Item)); } static partial void UpdateAllProjSpecific(float deltaTime) @@ -751,7 +758,7 @@ namespace Barotrauma //started moving the selected entities if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) { - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { SpriteEffects spriteEffects = SpriteEffects.None; switch (e) @@ -800,7 +807,32 @@ namespace Barotrauma } if (selectionPos != null && selectionPos != Vector2.Zero) { - GUI.DrawRectangle(spriteBatch, new Vector2(selectionPos.X, -selectionPos.Y), selectionSize, Color.DarkRed, false, 0, 2f / GameScreen.Selected.Cam.Zoom); + var (sizeX, sizeY) = selectionSize; + var (posX, posY) = selectionPos; + + posY = -posY; + + Vector2[] corners = + { + new Vector2(posX, posY), + new Vector2(posX + sizeX, posY), + new Vector2(posX + sizeX, posY + sizeY), + new Vector2(posX, posY + sizeY) + }; + + Color selectionColor = GUI.Style.Blue; + float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); + + GUI.DrawFilledRectangle(spriteBatch, corners[0], selectionSize, selectionColor * 0.1f); + + Vector2 offset = new Vector2(0f, thickness / 2f); + + if (sizeY < 0) { offset.Y = -offset.Y; } + + spriteBatch.DrawLine(corners[0], corners[1], selectionColor, thickness); + spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, selectionColor, thickness); + spriteBatch.DrawLine(corners[2], corners[3], selectionColor, thickness); + spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, selectionColor, thickness); } } @@ -824,8 +856,8 @@ namespace Barotrauma } } FilteredSelectedList.Clear(); - if (selectedList.Count == 0) return; - foreach (var e in selectedList) + if (SelectedList.Count == 0) return; + foreach (var e in SelectedList) { if (e is Gap gap && gap.ConnectedDoor != null) { continue; } FilteredSelectedList.Add(e); @@ -844,15 +876,19 @@ namespace Barotrauma { if (PlayerInput.KeyHit(Keys.N)) { - float minX = selectedList[0].WorldRect.X, maxX = selectedList[0].WorldRect.Right; - for (int i = 0; i < selectedList.Count; i++) + MapEntity firstSelected = SelectedList.First(); + + float minX = firstSelected.WorldRect.X, + maxX = firstSelected.WorldRect.Right; + + foreach (MapEntity entity in SelectedList) { - minX = Math.Min(minX, selectedList[i].WorldRect.X); - maxX = Math.Max(maxX, selectedList[i].WorldRect.Right); + minX = Math.Min(minX, entity.WorldRect.X); + maxX = Math.Max(maxX, entity.WorldRect.Right); } float centerX = (minX + maxX) / 2.0f; - foreach (MapEntity me in selectedList) + foreach (MapEntity me in SelectedList) { me.FlipX(false); me.Move(new Vector2((centerX - me.WorldPosition.X) * 2.0f, 0.0f)); @@ -860,15 +896,20 @@ namespace Barotrauma } else if (PlayerInput.KeyHit(Keys.M)) { - float minY = selectedList[0].WorldRect.Y - selectedList[0].WorldRect.Height, maxY = selectedList[0].WorldRect.Y; - for (int i = 0; i < selectedList.Count; i++) + MapEntity firstSelected = SelectedList.First(); + + float minY = firstSelected.WorldRect.Y - firstSelected.WorldRect.Height, + maxY = firstSelected.WorldRect.Y; + + foreach (MapEntity entity in SelectedList) { - minY = Math.Min(minY, selectedList[i].WorldRect.Y - selectedList[i].WorldRect.Height); - maxY = Math.Max(maxY, selectedList[i].WorldRect.Y); + + minY = Math.Min(minY, entity.WorldRect.Y - entity.WorldRect.Height); + maxY = Math.Max(maxY, entity.WorldRect.Y); } float centerY = (minY + maxY) / 2.0f; - foreach (MapEntity me in selectedList) + foreach (MapEntity me in SelectedList) { me.FlipY(false); me.Move(new Vector2(0.0f, (centerY - me.WorldPosition.Y) * 2.0f)); @@ -879,19 +920,20 @@ namespace Barotrauma public static void DrawEditor(SpriteBatch spriteBatch, Camera cam) { - if (selectedList.Count == 1) + if (SelectedList.Count == 1) { - selectedList[0].DrawEditing(spriteBatch, cam); - if (selectedList[0].ResizeHorizontal || selectedList[0].ResizeVertical) + MapEntity firstSelected = SelectedList.First(); + firstSelected.DrawEditing(spriteBatch, cam); + if (firstSelected.ResizeHorizontal || firstSelected.ResizeVertical) { - selectedList[0].DrawResizing(spriteBatch, cam); + firstSelected.DrawResizing(spriteBatch, cam); } } } public static void DeselectAll() { - selectedList.Clear(); + SelectedList.Clear(); } public static void SelectEntity(MapEntity entity) @@ -926,10 +968,10 @@ namespace Barotrauma public static void Paste(Vector2 position) { - if (copiedList.Count == 0) { return; } + if (CopiedList.Count == 0) { return; } List prevEntities = new List(mapEntityList); - Clone(copiedList); + Clone(CopiedList); var clones = mapEntityList.Except(prevEntities).ToList(); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); @@ -941,8 +983,8 @@ namespace Barotrauma Vector2 moveAmount = Submarine.VectorToWorldGrid(position - center); - selectedList = new List(clones); - foreach (MapEntity clone in selectedList) + SelectedList = new HashSet(clones); + foreach (MapEntity clone in SelectedList) { clone.Move(moveAmount); clone.Submarine = Submarine.MainSub; @@ -958,7 +1000,7 @@ namespace Barotrauma { List prevEntities = new List(mapEntityList); - copiedList = Clone(entities); + CopiedList = Clone(entities); //find all new entities created during cloning var newEntities = mapEntityList.Except(prevEntities).ToList(); @@ -1131,9 +1173,9 @@ namespace Barotrauma /// /// Find entities whose rect intersects with the "selection rect" /// - public static List FindSelectedEntities(Vector2 pos, Vector2 size) + public static HashSet FindSelectedEntities(Vector2 pos, Vector2 size) { - List foundEntities = new List(); + HashSet foundEntities = new HashSet(); Rectangle selectionRect = Submarine.AbsRect(pos, size); @@ -1141,7 +1183,11 @@ namespace Barotrauma { if (!e.SelectableInEditor) continue; - if (Submarine.RectsOverlap(selectionRect, e.rect)) foundEntities.Add(e); + if (Submarine.RectsOverlap(selectionRect, e.rect)) + { + foundEntities.Add(e); + e.IsIncludedInSelection = true; + } } return foundEntities; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 9d4200691..16fedb53c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -127,7 +127,11 @@ namespace Barotrauma ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => { - FlipX(relativeToSub: false); + foreach (MapEntity me in SelectedList) + { + me.FlipX(relativeToSub: false); + } + if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } return true; } }; @@ -136,7 +140,11 @@ namespace Barotrauma ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => { - FlipY(relativeToSub: false); + foreach (MapEntity me in SelectedList) + { + me.FlipY(relativeToSub: false); + } + if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } return true; } }; @@ -145,7 +153,7 @@ namespace Barotrauma OnClicked = (button, data) => { Sprite.ReloadXML(); - Sprite.ReloadTexture(); + Sprite.ReloadTexture(updateAllSprites: true); return true; } }; @@ -153,7 +161,12 @@ namespace Barotrauma { OnClicked = (button, data) => { - Reset(); + foreach (MapEntity me in SelectedList) + { + (me as Item)?.Reset(); + (me as Structure)?.Reset(); + } + if (!SelectedList.Contains(this)) { Reset(); } CreateEditingHUD(); return true; } @@ -247,7 +260,7 @@ namespace Barotrauma } else if (HiddenInGame) { return; } - Color color = IsHighlighted ? GUI.Style.Orange : spriteColor; + Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted ? GUI.Style.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; if (IsSelected && editing) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 412dcad18..95c090122 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -114,7 +114,6 @@ namespace Barotrauma if (realWorldDimensions != Vector2.Zero) { string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); - var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; @@ -124,6 +123,15 @@ namespace Barotrauma dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); } + string cargoCapacityStr = CargoCapacity < 0 ? TextManager.Get("unknown") : TextManager.GetWithVariables("cargocapacityformat", new string[1] { "[cratecount]" }, new string[1] {CargoCapacity.ToString() }); + var cargoCapacityText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), + TextManager.Get("cargocapacity"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), cargoCapacityText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + cargoCapacityStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + cargoCapacityText.RectTransform.MinSize = new Point(0, cargoCapacityText.Children.First().Rect.Height); + if (RecommendedCrewSizeMax > 0) { var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index a616356c8..8d9bf6343 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -46,7 +46,10 @@ namespace Barotrauma if (IsHighlighted || IsHighlighted) { clr = Color.Lerp(clr, Color.White, 0.8f); } int iconSize = spawnType == SpawnType.Path ? WaypointSize : SpawnPointSize; - if (ConnectedDoor != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { iconSize = (int)(iconSize * 1.5f); } + if (ConnectedDoor != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) + { + iconSize = (int)(iconSize * 1.5f); + } if (IsSelected || IsHighlighted) { @@ -98,10 +101,32 @@ namespace Barotrauma GUI.Style.Green * 0.5f, width: 1); } + var color = Color.WhiteSmoke; + if (spawnType == SpawnType.Path) + { + if (linkedTo.Count < 2) + { + if (linkedTo.Count == 0) + { + color = Color.Red; + } + else + { + if (CurrentHull == null) + { + color = Ladders == null ? Color.Red : Color.Yellow; + } + else + { + color = Color.Yellow; + } + } + } + } GUI.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), - Color.WhiteSmoke); + color); } public override bool IsMouseOn(Vector2 position) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 3a94e246e..feee46eb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -53,100 +53,32 @@ namespace Barotrauma.Networking case ChatMessageType.Default: break; case ChatMessageType.Order: - int orderIndex = msg.ReadByte(); - UInt16 targetCharacterID = msg.ReadUInt16(); - Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; - Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); - - Order orderPrefab = null; - int? optionIndex = null; - string orderOption = null; - - // The option of a Dismiss order is written differently so we know what order we target - // now that the game supports multiple current orders simultaneously - if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) - { - orderPrefab = Order.PrefabList[orderIndex]; - if (orderPrefab.Identifier != "dismissed") - { - optionIndex = msg.ReadByte(); - } - // Does the dismiss order have a specified target? - else if (msg.ReadBoolean()) - { - int identifierCount = msg.ReadByte(); - if (identifierCount > 0) - { - int dismissedOrderIndex = msg.ReadByte(); - Order dismissedOrderPrefab = null; - if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) - { - dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; - orderOption = dismissedOrderPrefab.Identifier; - } - if (identifierCount > 1) - { - int dismissedOrderOptionIndex = msg.ReadByte(); - if (dismissedOrderPrefab != null) - { - var options = dismissedOrderPrefab.Options; - if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) - { - orderOption += $".{options[dismissedOrderOptionIndex]}"; - } - } - } - } - } - } - else - { - optionIndex = msg.ReadByte(); - } - - int orderPriority = msg.ReadByte(); - OrderTarget orderTargetPosition = null; - Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); - int wallSectionIndex = 0; - if (msg.ReadBoolean()) - { - var x = msg.ReadSingle(); - var y = msg.ReadSingle(); - var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; - orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); - } - else if(orderTargetType == Order.OrderTargetType.WallSection) - { - wallSectionIndex = msg.ReadByte(); - } - - if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) + var orderMessageInfo = OrderChatMessage.ReadOrder(msg); + if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); if (NetIdUtils.IdMoreRecent(id, LastID)) { LastID = id; } return; } - else - { - orderPrefab ??= Order.PrefabList[orderIndex]; - } - - orderOption ??= optionIndex.HasValue && optionIndex >= 0 && optionIndex < orderPrefab.Options.Length ? orderPrefab.Options[optionIndex.Value] : ""; - txt = orderPrefab.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); + var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; + string orderOption = orderMessageInfo.OrderOption; + orderOption ??= orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length ? + orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] : ""; + txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, orderOption: orderOption); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { Order order = null; - switch (orderTargetType) + switch (orderMessageInfo.TargetType) { case Order.OrderTargetType.Entity: - order = new Order(orderPrefab, targetEntity, orderPrefab.GetTargetItemComponent(targetEntity as Item), orderGiver: senderCharacter); + order = new Order(orderPrefab, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter); break; case Order.OrderTargetType.Position: - order = new Order(orderPrefab, orderTargetPosition, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); break; case Order.OrderTargetType.WallSection: - order = new Order(orderPrefab, targetEntity as Structure, wallSectionIndex, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter); break; } @@ -157,9 +89,9 @@ namespace Barotrauma.Networking var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null; GameMain.GameSession?.CrewManager?.AddOrder(order, fadeOutTime); } - else if (targetCharacter != null) + else { - targetCharacter.SetOrder(order, orderOption, orderPriority, senderCharacter); + orderMessageInfo.TargetCharacter?.SetOrder(order, orderOption, orderMessageInfo.Priority, senderCharacter); } } } @@ -167,7 +99,7 @@ namespace Barotrauma.Networking if (NetIdUtils.IdMoreRecent(id, LastID)) { GameMain.Client.AddChatMessage( - new OrderChatMessage(orderPrefab, orderOption, orderPriority, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); + new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, txt, orderMessageInfo.TargetPosition ?? orderMessageInfo.TargetEntity as ISpatialEntity, orderMessageInfo.TargetCharacter, senderCharacter)); LastID = id; } return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 4cf5cde4c..363d909a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -527,7 +527,7 @@ namespace Barotrauma.Networking string pwMsg = TextManager.Get("PasswordRequired"); var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, (int)(170 * Math.Max(1.0f, GUI.Scale)))); + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) { @@ -537,7 +537,8 @@ namespace Barotrauma.Networking if (wrongPassword) { - new GUITextBlock(new RectTransform(new Vector2(1f, 0.33f), passwordHolder.RectTransform), TextManager.Language == "English" ? TextManager.Get("incorrectpassword") : "Incorrect password", GUI.Style.Red, GUI.Font, textAlignment: Alignment.Center); + var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUI.Style.Red, GUI.Font, textAlignment: Alignment.Center); + incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); passwordHolder.Recalculate(); } @@ -643,7 +644,7 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Error while reading a message from server.", e); new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", new string[2] { "[message]", "[targetsite]" }, new string[2] { e.Message, e.TargetSite.ToString() })); Disconnect(); - GameMain.MainMenuScreen.Select(); + GameMain.ServerListScreen.Select(); return; } @@ -659,10 +660,7 @@ namespace Barotrauma.Networking { EndVoteTickBox.Visible = serverSettings.Voting.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); - if (respawnManager != null) - { - respawnManager.Update(deltaTime); - } + respawnManager?.Update(deltaTime); if (updateTimer <= DateTime.Now) { @@ -936,9 +934,6 @@ namespace Barotrauma.Networking } } break; - case ServerPacketHeader.RESET_UPGRADES: - campaign?.UpgradeManager.ClientRead(inc); - break; case ServerPacketHeader.CREW: campaign?.ClientReadCrew(inc); break; @@ -993,15 +988,21 @@ namespace Barotrauma.Networking string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; throw new Exception(errorMsg); } - foreach (Mission mission in GameMain.GameSession.Missions) + List serverMissionIdentifiers = new List(); + for (int i = 0; i < missionCount; i++) { - string missionIdentifier = inc.ReadString() ?? ""; - if (missionIdentifier != mission.Prefab.Identifier) + serverMissionIdentifiers.Add(inc.ReadString() ?? ""); + } + + if (missionCount > 0) + { + if (!GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier).OrderBy(id => id).SequenceEqual(serverMissionIdentifiers.OrderBy(id => id))) { - string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {mission.Prefab.Identifier})"; + string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {string.Join(", ", serverMissionIdentifiers)}, client: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } + GameMain.GameSession.EnforceMissionOrder(serverMissionIdentifiers); } byte equalityCheckValueCount = inc.ReadByte(); @@ -1046,6 +1047,11 @@ namespace Barotrauma.Networking mission.ClientReadInitial(inc); } + if (inc.ReadBoolean()) + { + CrewManager.ClientReadActiveOrders(inc); + } + roundInitStatus = RoundInitStatus.Started; } @@ -1308,10 +1314,7 @@ namespace Barotrauma.Networking Client.ReadPermissions(inc, out permissions, out permittedCommands); Client targetClient = ConnectedClients.Find(c => c.ID == clientID); - if (targetClient != null) - { - targetClient.SetPermissions(permissions, permittedCommands); - } + targetClient?.SetPermissions(permissions, permittedCommands); if (clientID == myID) { SetMyPermissions(permissions, permittedCommands.Select(command => command.names[0])); @@ -1422,7 +1425,7 @@ namespace Barotrauma.Networking while (CoroutineManager.IsCoroutineRunning("EndGame")) { - if (EndCinematic != null) { EndCinematic.Stop(); } + EndCinematic?.Stop(); yield return CoroutineStatus.Running; } @@ -1609,6 +1612,9 @@ namespace Barotrauma.Networking DateTime? timeOut = null; DateTime requestFinalizeTime = DateTime.Now; TimeSpan requestFinalizeInterval = new TimeSpan(0, 0, 2); + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); + clientPeer.Send(msg, DeliveryMethod.Unreliable); while (true) { @@ -1618,7 +1624,7 @@ namespace Barotrauma.Networking { if (DateTime.Now > requestFinalizeTime) { - IWriteMessage msg = new WriteOnlyMessage(); + msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.REQUEST_STARTGAMEFINALIZE); clientPeer.Send(msg, DeliveryMethod.Unreliable); requestFinalizeTime = DateTime.Now + requestFinalizeInterval; @@ -1648,12 +1654,11 @@ namespace Barotrauma.Networking break; } - if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) - { - break; - } + if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } clientPeer.Update((float)Timing.Step); + + if (roundInitStatus != RoundInitStatus.WaitingForStartGameFinalize) { break; } } catch (Exception e) { @@ -2089,6 +2094,7 @@ namespace Barotrauma.Networking float autoRestartTimer = autoRestartEnabled ? inc.ReadSingle() : 0.0f; bool radiationEnabled = inc.ReadBoolean(); + byte maxMissionCount = inc.ReadByte(); //ignore the message if we already a more up-to-date one //or if we're still waiting for the initial update @@ -2154,6 +2160,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetRadiationEnabled(radiationEnabled); GameMain.NetLobbyScreen.SetBotSpawnMode(botSpawnMode); GameMain.NetLobbyScreen.SetBotCount(botCount); + GameMain.NetLobbyScreen.SetMaxMissionCount(maxMissionCount); GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); serverSettings.VoiceChatEnabled = voiceChatEnabled; @@ -2752,6 +2759,8 @@ namespace Barotrauma.Networking public void Vote(VoteType voteType, object data) { + if (clientPeer == null) return; + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); msg.Write((byte)ClientNetObject.VOTE); @@ -3289,16 +3298,24 @@ namespace Barotrauma.Networking { string respawnText = string.Empty; Color textColor = Color.White; - bool canChooseRespawn = - GameMain.GameSession.GameMode is CampaignMode && - Character.Controlled == null && + bool canChooseRespawn = + GameMain.GameSession.GameMode is CampaignMode && + Character.Controlled == null && Level.Loaded?.Type != LevelData.LevelType.Outpost && (characterInfo == null || HasSpawned); - if (respawnManager.CurrentState == RespawnManager.State.Waiting && - respawnManager.RespawnCountdownStarted) + if (respawnManager.CurrentState == RespawnManager.State.Waiting) { - float timeLeft = (float)(respawnManager.RespawnTime - DateTime.Now).TotalSeconds; - respawnText = TextManager.GetWithVariable(respawnManager.UsingShuttle ? "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); + if (respawnManager.RespawnCountdownStarted) + { + float timeLeft = (float)(respawnManager.RespawnTime - DateTime.Now).TotalSeconds; + respawnText = TextManager.GetWithVariable(respawnManager.UsingShuttle ? "RespawnShuttleDispatching" : "RespawningIn", "[time]", ToolBox.SecondsToReadableTime(timeLeft)); + } + else if (respawnManager.PendingRespawnCount > 0) + { + respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", + new string[] { "[deadplayers]", "[requireddeadplayers]" }, + new string[] { respawnManager.PendingRespawnCount.ToString(), respawnManager.RequiredRespawnCount.ToString() }); + } } else if (respawnManager.CurrentState == RespawnManager.State.Transporting && respawnManager.ReturnCountdownStarted) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index aad9283e8..cc3910164 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -1,6 +1,4 @@ -using Lidgren.Network; -using Microsoft.Xna.Framework; -using System; +using System; namespace Barotrauma.Networking { @@ -8,6 +6,17 @@ namespace Barotrauma.Networking { private DateTime lastShuttleLeavingWarningTime; + public int PendingRespawnCount + + { + get; private set; + } + + public int RequiredRespawnCount + { + get; private set; + } + partial void UpdateTransportingProjSpecific(float deltaTime) { if (GameMain.Client?.Character == null || GameMain.Client.Character.Submarine != RespawnShuttle) { return; } @@ -41,6 +50,8 @@ namespace Barotrauma.Networking } break; case State.Waiting: + PendingRespawnCount = msg.ReadUInt16(); + RequiredRespawnCount = msg.ReadUInt16(); RespawnCountdownStarted = msg.ReadBoolean(); ResetShuttle(); float newRespawnTime = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 341085ff1..07f3a2f9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -151,7 +151,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? radiationEnabled = null, bool? useRespawnShuttle = null) + public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? radiationEnabled = null, bool? useRespawnShuttle = null, int maxMissionCount = 0) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -217,6 +217,8 @@ namespace Barotrauma.Networking outMsg.Write(autoRestart != null); outMsg.Write(autoRestart ?? false); outMsg.Write(radiationEnabled ?? RadiationEnabled); + outMsg.Write((byte)maxMissionCount + 1); + outMsg.WritePadBits(); } @@ -745,6 +747,10 @@ namespace Barotrauma.Networking TextManager.Get("ServerSettingsAllowRewiring")); GetPropertyData("AllowRewiring").AssignGUIComponent(allowRewiring); + var allowWifiChatter = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), + TextManager.Get("ServerSettingsAllowWifiChat")); + GetPropertyData("AllowLinkingWifiToChat").AssignGUIComponent(allowWifiChatter); + var allowDisguises = new GUITickBox(new RectTransform(new Vector2(0.48f, 0.05f), tickBoxContainer.Content.RectTransform), TextManager.Get("ServerSettingsAllowDisguises")); GetPropertyData("AllowDisguises").AssignGUIComponent(allowDisguises); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 55dc6e385..3a4852708 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -26,8 +26,6 @@ namespace Barotrauma.Particles private float angularVelocity; - private Vector2 dragVec = Vector2.Zero; - private float dragWait = 0; private float collisionIgnoreTimer = 0; private Vector2 size; @@ -35,6 +33,7 @@ namespace Barotrauma.Particles private Color color; private bool changeColor; + private bool UseMiddleColor; private int spriteIndex; @@ -66,7 +65,7 @@ namespace Barotrauma.Particles public Vector4 ColorMultiplier; public bool DrawOnTop { get; private set; } - + public ParticlePrefab.DrawTargetType DrawTarget { get { return prefab.DrawTarget; } @@ -103,8 +102,7 @@ namespace Barotrauma.Particles { return debugName; } - - public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f) + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { this.prefab = prefab; debugName = $"Particle ({prefab.Name})"; @@ -113,11 +111,19 @@ namespace Barotrauma.Particles animState = 0; animFrame = 0; - dragWait = 0; - dragVec = Vector2.Zero; - + currentHull = Hull.FindHull(position, hullGuess); + size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); + + if (tracerPoints != null) + { + size = new Vector2(Vector2.Distance(tracerPoints.Item1, tracerPoints.Item2), size.Y); + position = (tracerPoints.Item1 + tracerPoints.Item2) / 2; + } + + sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f); + this.position = position; prevPosition = position; @@ -135,14 +141,21 @@ namespace Barotrauma.Particles angularVelocity = Rand.Range(prefab.AngularVelocityMinRad, prefab.AngularVelocityMaxRad); - totalLifeTime = prefab.LifeTime; - lifeTime = prefab.LifeTime; + + if (prefab.LifeTimeMin <= 0.0f) + { + totalLifeTime = prefab.LifeTime; + lifeTime = prefab.LifeTime; + } + else + { + totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime); + lifeTime = totalLifeTime; + } + startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax); - - size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); - - sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f); + UseMiddleColor = prefab.UseMiddleColor; color = prefab.StartColor; changeColor = prefab.StartColor != prefab.EndColor; ColorMultiplier = Vector4.One; @@ -238,13 +251,27 @@ namespace Barotrauma.Particles } size.X += sizeChange.X * deltaTime; - size.Y += sizeChange.Y * deltaTime; + size.Y += sizeChange.Y * deltaTime; - if (changeColor) + if (UseMiddleColor) { - color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / prefab.LifeTime); + if (lifeTime > totalLifeTime * 0.5f) + { + color = Color.Lerp(prefab.MiddleColor, prefab.StartColor, (lifeTime / totalLifeTime - 0.5f) * 2.0f); + } + else + { + color = Color.Lerp(prefab.EndColor, prefab.MiddleColor, lifeTime / totalLifeTime * 2.0f); + } } - + else + { + if (changeColor) + { + color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / totalLifeTime); + } + } + if (prefab.Sprites[spriteIndex] is SpriteSheet) { animState += deltaTime; @@ -394,29 +421,35 @@ namespace Barotrauma.Particles private void ApplyDrag(float dragCoefficient, float deltaTime) { - if (velocity.LengthSquared() < dragVec.LengthSquared()) + Vector2 relativeVel = velocity; + if (currentHull?.Submarine != null) { - velocity = Vector2.Zero; - return; - } - if (Math.Abs(velocity.X) < 0.0001f && Math.Abs(velocity.Y) < 0.0001f) return; - - //TODO: some better way to handle particle drag - //this doesn't work that well because the drag vector is only updated every 0.5 seconds, allowing the particle to accelerate way more than it should - //(e.g. a falling particle can freely accelerate for 0.5 seconds before the drag takes effect) - dragWait-=deltaTime; - if (dragWait <= 0f) - { - dragWait = 0.5f; - - float speed = velocity.Length(); - - dragVec = (velocity / speed) * Math.Min(speed * speed * dragCoefficient * deltaTime, 1.0f); + relativeVel = velocity - ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); } - velocity -= dragVec; + float speed = relativeVel.Length(); + + relativeVel /= speed; + + float drag = speed * speed * dragCoefficient * 0.01f * deltaTime; + if (drag > speed) + { + relativeVel = Vector2.Zero; + } + else + { + speed -= drag; + relativeVel *= speed; + } + + velocity = relativeVel; + if (currentHull?.Submarine != null) + { + velocity += ConvertUnits.ToDisplayUnits(currentHull.Submarine.Velocity); + } } + private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal) { if (prevHull == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 0335ba0c7..f54793630 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -1,14 +1,127 @@ using Microsoft.Xna.Framework; using System; -using System.Linq; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Particles { + class ParticleEmitterProperties : ISerializableEntity + { + private const float MinValue = int.MinValue, + MaxValue = int.MaxValue; + + public string Name => nameof(ParticleEmitter); + + private float angleMin, angleMax; + + public float AngleMinRad { get; private set; } + public float AngleMaxRad { get; private set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + public float AngleMin + { + get => angleMin; + set + { + angleMin = value; + AngleMinRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f)); + } + } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + public float AngleMax + { + get => angleMax; + set + { + angleMax = value; + AngleMaxRad = MathHelper.ToRadians(MathHelper.Clamp(value, -360.0f, 360.0f)); + } + } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + public float DistanceMin { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + public float DistanceMax { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + public float VelocityMin { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + public float VelocityMax { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + public float ScaleMin { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + public float ScaleMax { get; set; } + + + [Editable(), Serialize("1,1", true)] + public Vector2 ScaleMultiplier { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + public float EmitInterval { get; set; } + + [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, true, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] + public int ParticleAmount { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + public float ParticlesPerSecond { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] + public float EmitAcrossRayInterval { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "Delay before the emitter becomes active after being created.")] + public float InitialDelay { get; set; } + + [Editable, Serialize(false, true)] + public bool HighQualityCollisionDetection { get; set; } + + [Editable, Serialize(false, true)] + public bool CopyEntityAngle { get; set; } + + [Editable, Serialize("1,1,1,1", true)] + public Color ColorMultiplier { get; set; } + + [Editable, Serialize(false, true)] + public bool DrawOnTop { get; set; } + + [Serialize(0f, true)] + public float Angle + { + get => AngleMin; + set => AngleMin = AngleMax = value; + } + + [Serialize(0f, true)] + public float Distance + { + get => DistanceMin; + set => DistanceMin = DistanceMax = value; + } + + [Serialize(0f, true)] + public float Velocity + { + get => VelocityMin; + set => VelocityMin = VelocityMax = value; + } + + public Dictionary SerializableProperties { get; } + + public ParticleEmitterProperties(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + class ParticleEmitter { private float emitTimer; private float burstEmitTimer; + private float initialDelay; public readonly ParticleEmitterPrefab Prefab; @@ -23,51 +136,76 @@ namespace Barotrauma.Particles Prefab = prefab; } - public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null) + public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) { + if (initialDelay < Prefab.Properties.InitialDelay) + { + initialDelay += deltaTime; + return; + } + emitTimer += deltaTime * amountMultiplier; burstEmitTimer -= deltaTime; - if (Prefab.ParticlesPerSecond > 0) + if (Prefab.Properties.EmitAcrossRayInterval > 0.0f && tracerPoints != null) { - float emitInterval = 1.0f / Prefab.ParticlesPerSecond; + Vector2 dir = tracerPoints.Item2 - tracerPoints.Item1; + if (dir.LengthSquared() > 0.001f) + { + float dist = dir.Length(); + dir /= dist; + for (float z = 0.0f; z < dist; z += Prefab.Properties.EmitAcrossRayInterval) + { + Vector2 pos = tracerPoints.Item1 + dir * z; + Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: null); + } + } + } + + if (Prefab.Properties.ParticlesPerSecond > 0) + { + float emitInterval = 1.0f / Prefab.Properties.ParticlesPerSecond; while (emitTimer > emitInterval) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: tracerPoints); emitTimer -= emitInterval; } } if (burstEmitTimer > 0.0f) { return; } - - burstEmitTimer = Prefab.EmitInterval; - for (int i = 0; i < Prefab.ParticleAmount * amountMultiplier; i++) + + burstEmitTimer = Prefab.Properties.EmitInterval; + for (int i = 0; i < Prefab.Properties.ParticleAmount * amountMultiplier; i++) { - Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle); + Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: tracerPoints); } } - private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null) + private void Emit(Vector2 position, Hull hullGuess, float angle, float particleRotation, float velocityMultiplier, float sizeMultiplier, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) { - angle += Rand.Range(Prefab.AngleMin, Prefab.AngleMax); + var particlePrefab = overrideParticle ?? Prefab.ParticlePrefab; + if (particlePrefab == null) { return; } + + angle += Rand.Range(Prefab.Properties.AngleMinRad, Prefab.Properties.AngleMaxRad); Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); - Vector2 velocity = dir * Rand.Range(Prefab.VelocityMin, Prefab.VelocityMax) * velocityMultiplier; - position += dir * Rand.Range(Prefab.DistanceMin, Prefab.DistanceMax); + Vector2 velocity = dir * Rand.Range(Prefab.Properties.VelocityMin, Prefab.Properties.VelocityMax) * velocityMultiplier; + position += dir * Rand.Range(Prefab.Properties.DistanceMin, Prefab.Properties.DistanceMax); - var particle = GameMain.ParticleManager.CreateParticle(overrideParticle ?? Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop); + var particle = GameMain.ParticleManager.CreateParticle(particlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, tracerPoints: tracerPoints); if (particle != null) { - particle.Size *= Rand.Range(Prefab.ScaleMin, Prefab.ScaleMax) * sizeMultiplier; - particle.HighQualityCollisionDetection = Prefab.HighQualityCollisionDetection; - if (colorMultiplier.HasValue) - { - particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); - } - else if (Prefab.ColorMultiplier != Color.White) + particle.Size *= Rand.Range(Prefab.Properties.ScaleMin, Prefab.Properties.ScaleMax) * sizeMultiplier; + particle.Size *= Prefab.Properties.ScaleMultiplier; + particle.HighQualityCollisionDetection = Prefab.Properties.HighQualityCollisionDetection; + if (colorMultiplier.HasValue) { - particle.ColorMultiplier = Prefab.ColorMultiplier.ToVector4(); + particle.ColorMultiplier = colorMultiplier.Value.ToVector4(); + } + else if (Prefab.Properties.ColorMultiplier != Color.White) + { + particle.ColorMultiplier = Prefab.Properties.ColorMultiplier.ToVector4(); } } } @@ -76,9 +214,9 @@ namespace Barotrauma.Particles { Rectangle bounds = new Rectangle((int)startPosition.X, (int)startPosition.Y, (int)startPosition.X, (int)startPosition.Y); - for (float angle = Prefab.AngleMin; angle <= Prefab.AngleMax; angle += 0.1f) + for (float angle = Prefab.Properties.AngleMinRad; angle <= Prefab.Properties.AngleMaxRad; angle += 0.1f) { - Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.VelocityMax; + Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Prefab.Properties.VelocityMax; Vector2 endPosition = Prefab.ParticlePrefab.CalculateEndPosition(startPosition, velocity); Vector2 endSize = Prefab.ParticlePrefab.CalculateEndSize(); @@ -91,15 +229,15 @@ namespace Barotrauma.Particles } else { - spriteExtent = Math.Max(spriteExtent, Math.Max(sprite.size.X * endSize.X, sprite.size.Y * endSize.Y)); + spriteExtent = Math.Max(spriteExtent, Math.Max(sprite.size.X * endSize.X, sprite.size.Y * endSize.Y)); } } bounds = new Rectangle( - (int)Math.Min(bounds.X, endPosition.X - Prefab.DistanceMax - spriteExtent / 2), - (int)Math.Min(bounds.Y, endPosition.Y - Prefab.DistanceMax - spriteExtent / 2), - (int)Math.Max(bounds.X, endPosition.X + Prefab.DistanceMax + spriteExtent / 2), - (int)Math.Max(bounds.Y, endPosition.Y + Prefab.DistanceMax + spriteExtent / 2)); + (int)Math.Min(bounds.X, endPosition.X - Prefab.Properties.DistanceMax - spriteExtent / 2), + (int)Math.Min(bounds.Y, endPosition.Y - Prefab.Properties.DistanceMax - spriteExtent / 2), + (int)Math.Max(bounds.X, endPosition.X + Prefab.Properties.DistanceMax + spriteExtent / 2), + (int)Math.Max(bounds.Y, endPosition.Y + Prefab.Properties.DistanceMax + spriteExtent / 2)); } bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - bounds.X, bounds.Height - bounds.Y); @@ -109,9 +247,7 @@ namespace Barotrauma.Particles } class ParticleEmitterPrefab - { - public readonly string Name; - + { private string particlePrefabName; private ParticlePrefab particlePrefab; @@ -122,103 +258,30 @@ namespace Barotrauma.Particles if (particlePrefab == null && particlePrefabName != null) { particlePrefab = GameMain.ParticleManager?.FindPrefab(particlePrefabName); - if (particlePrefab == null) { particlePrefabName = null; } + if (particlePrefab == null) + { + DebugConsole.ThrowError($"Failed to find particle prefab \"{particlePrefabName}\"."); + particlePrefabName = null; + } } return particlePrefab; } } - public readonly float AngleMin, AngleMax; + public readonly ParticleEmitterProperties Properties; - public readonly float DistanceMin, DistanceMax; - - public readonly float VelocityMin, VelocityMax; - - public readonly float ScaleMin, ScaleMax; - - public readonly float EmitInterval; - public readonly int ParticleAmount; - - public readonly float ParticlesPerSecond; - - public readonly bool HighQualityCollisionDetection; - - public readonly bool CopyEntityAngle; - - public readonly Color ColorMultiplier; - - public bool DrawOnTop => forceDrawOnTop || ParticlePrefab.DrawOnTop; - private readonly bool forceDrawOnTop; + public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab.DrawOnTop; public ParticleEmitterPrefab(XElement element) { - Name = element.Name.ToString(); + Properties = new ParticleEmitterProperties(element); particlePrefabName = element.GetAttributeString("particle", ""); + } - if (element.Attribute("startrotation") == null) - { - AngleMin = element.GetAttributeFloat("anglemin", 0.0f); - AngleMax = element.GetAttributeFloat("anglemax", 0.0f); - } - else - { - AngleMin = element.GetAttributeFloat("angle", 0.0f); - AngleMax = AngleMin; - } - - AngleMin = MathHelper.ToRadians(MathHelper.Clamp(AngleMin, -360.0f, 360.0f)); - AngleMax = MathHelper.ToRadians(MathHelper.Clamp(AngleMax, -360.0f, 360.0f)); - - if (element.Attribute("scalemin") == null) - { - ScaleMin = 1.0f; - ScaleMax = 1.0f; - } - else - { - ScaleMin = element.GetAttributeFloat("scalemin", 1.0f); - ScaleMax = Math.Max(ScaleMin, element.GetAttributeFloat("scalemax", 1.0f)); - } - - if (element.Attribute("distance") == null) - { - DistanceMin = element.GetAttributeFloat("distancemin", 0.0f); - DistanceMax = element.GetAttributeFloat("distancemax", 0.0f); - } - else - { - DistanceMin = DistanceMax = element.GetAttributeFloat("distance", 0.0f); - } - if (DistanceMax < DistanceMin) - { - var temp = DistanceMin; - DistanceMin = DistanceMax; - DistanceMax = temp; - } - - if (element.Attribute("velocity") == null) - { - VelocityMin = element.GetAttributeFloat("velocitymin", 0.0f); - VelocityMax = element.GetAttributeFloat("velocitymax", 0.0f); - } - else - { - VelocityMin = VelocityMax = element.GetAttributeFloat("velocity", 0.0f); - } - if (VelocityMax < VelocityMin) - { - var temp = VelocityMin; - VelocityMin = VelocityMax; - VelocityMax = temp; - } - - EmitInterval = element.GetAttributeFloat("emitinterval", 0.0f); - ParticlesPerSecond = element.GetAttributeFloat("particlespersecond", 0); - ParticleAmount = element.GetAttributeInt("particleamount", 0); - HighQualityCollisionDetection = element.GetAttributeBool("highqualitycollisiondetection", false); - CopyEntityAngle = element.GetAttributeBool("copyentityangle", false); - forceDrawOnTop = element.GetAttributeBool("drawontop", false); - ColorMultiplier = element.GetAttributeColor("colormultiplier", Color.White); + public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) + { + Properties = properties; + particlePrefab = prefab; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index bfc43ae7b..9ce02308e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -114,13 +114,12 @@ namespace Barotrauma.Particles { Prefabs.RemoveByFile(configFile); } - - public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f) + public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { - return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer); + return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer, tracerPoints: tracerPoints); } - public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f) + public Particle CreateParticle(string prefabName, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { ParticlePrefab prefab = FindPrefab(prefabName); @@ -129,27 +128,30 @@ namespace Barotrauma.Particles DebugConsole.ThrowError("Particle prefab \"" + prefabName + "\" not found!"); return null; } - - return CreateParticle(prefab, position, velocity, rotation, hullGuess, collisionIgnoreTimer: collisionIgnoreTimer); + 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) + public Particle CreateParticle(ParticlePrefab prefab, Vector2 position, Vector2 velocity, float rotation = 0.0f, Hull hullGuess = null, bool drawOnTop = false, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { if (particleCount >= MaxParticles || prefab == null || prefab.Sprites.Count == 0) { return null; } + + // this should be optimized for tracers after prototyping + if (tracerPoints == null) + { + Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); - Vector2 particleEndPos = prefab.CalculateEndPosition(position, velocity); + Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); + Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); - Vector2 minPos = new Vector2(Math.Min(position.X, particleEndPos.X), Math.Min(position.Y, particleEndPos.Y)); - Vector2 maxPos = new Vector2(Math.Max(position.X, particleEndPos.X), Math.Max(position.Y, particleEndPos.Y)); + Rectangle expandedViewRect = MathUtils.ExpandRect(cam.WorldView, MaxOutOfViewDist); - 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 (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); + particles[particleCount].Init(prefab, position, velocity, rotation, hullGuess, drawOnTop, collisionIgnoreTimer, tracerPoints: tracerPoints); particleCount++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index adfaeeaa9..104789a59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -53,6 +53,10 @@ namespace Barotrauma.Particles [Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")] public float LifeTime { get; private set; } + [Editable(0.0f, float.MaxValue), Serialize(0.0f, false, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] + public float LifeTimeMin { get; private set; } + + [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMin { get; private set; } [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] @@ -118,10 +122,10 @@ namespace Barotrauma.Particles [Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")] public bool RotateToDirection { get; private set; } - [Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] public float Drag { get; private set; } - [Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] public float WaterDrag { get; private set; } private Vector2 velocityChange; @@ -193,8 +197,14 @@ namespace Barotrauma.Particles [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] public Color StartColor { get; private set; } + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + public Color MiddleColor { get; private set; } + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")] public Color EndColor { get; private set; } + + [Editable, Serialize(false, false, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] + public bool UseMiddleColor { get; private set; } [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } @@ -289,7 +299,7 @@ namespace Barotrauma.Particles StartRotationMin = element.GetAttributeFloat("startrotation", 0.0f); StartRotationMax = StartRotationMin; } - + if (CollisionRadius <= 0.0f) CollisionRadius = Sprites.Count > 0 ? 1 : Sprites[0].SourceRect.Width / 2.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index a2a3d770e..d2ca4d04b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -443,7 +443,7 @@ namespace Barotrauma public static bool KeyHit(Keys button) { - return (AllowInput && oldKeyboardState.IsKeyDown(button) && keyboardState.IsKeyUp(button)); + return AllowInput && oldKeyboardState.IsKeyUp(button) && keyboardState.IsKeyDown(button); } public static bool InventoryKeyHit(int index) @@ -454,7 +454,7 @@ namespace Barotrauma public static bool KeyDown(Keys button) { - return (AllowInput && keyboardState.IsKeyDown(button)); + return AllowInput && keyboardState.IsKeyDown(button); } public static bool KeyUp(Keys button) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 19d9c2348..3d9cad979 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -6,6 +6,7 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using System.Globalization; +using Barotrauma.Extensions; namespace Barotrauma { @@ -42,6 +43,12 @@ namespace Barotrauma } public GUITickBox EnableRadiationToggle { get; set; } + public GUILayoutGroup CampaignSettingsContent { get; set; } + + public GUIButton CampaignCustomizeButton { get; set; } + public GUIMessageBox CampaignCustomizeSettings { get; set; } + + public GUITextBlock MaxMissionCountText; private readonly bool isMultiplayer; @@ -177,10 +184,19 @@ namespace Barotrauma if (isMultiplayer) { settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); + settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); } else { settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; + if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) + { + settings.MaxMissionCount = missionCount; + } + else + { + settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; + } } if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) @@ -265,14 +281,14 @@ namespace Barotrauma if (!isMultiplayer) { - if (MapGenerationParams.Instance.RadiationParams != null) + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { - EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 1f), buttonContainer.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + OnClicked = (tb, userdata) => { - Selected = true, - ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") - }; - } + CreateCustomizeWindow(); + return true; + } + }; var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") { @@ -290,6 +306,52 @@ namespace Barotrauma UpdateLoadMenu(saveFiles); } + private void CreateCustomizeWindow() + { + CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); + CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; + + CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) + { + RelativeSpacing = 0.1f + }; + + if (MapGenerationParams.Instance.RadiationParams != null) + { + EnableRadiationToggle = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.3f), CampaignSettingsContent.RectTransform), TextManager.Get("CampaignOption.EnableRadiation"), font: GUI.Style.Font) + { + Selected = true, + ToolTip = TextManager.Get("campaignoption.enableradiation.tooltip") + }; + } + var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), CampaignSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + ToolTip = TextManager.Get("maxmissioncounttooltip") + }; + var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true); + var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + var maxMissionCountButtons = new GUIButton[2]; + maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + return true; + } + }; + MaxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), CampaignSettings.DefaultMaxMissionCount.ToString(), textAlignment: Alignment.Center, style: "GUITextBox"); + maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 0.8f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + return true; + } + }; + maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); + } + private void CreateMultiplayerCampaignSubList(RectTransform parent) { GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 92698cec8..3c55ebd3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -21,6 +21,9 @@ namespace Barotrauma private GUIComponent locationInfoPanel; private GUIListBox missionList; + private readonly List missionTickBoxes = new List(); + + private bool hasMaxMissions; private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; @@ -51,9 +54,18 @@ namespace Barotrauma CreateUI(container); campaign.Map.OnLocationSelected += SelectLocation; - campaign.Map.OnMissionSelected += (connection, mission) => + campaign.Map.OnMissionsSelected += (connection, missions) => { - missionList?.Select(mission); + if (missionList?.Content != null) + { + foreach (GUIComponent missionElement in missionList.Content.Children) + { + if (missionElement.FindChild(c => c is GUITickBox, recursive: true) is GUITickBox tickBox) + { + tickBox.Selected = missions.Contains(tickBox.UserData as Mission); + } + } + } }; } @@ -311,6 +323,20 @@ namespace Barotrauma map.SelectLocation(-1); } map.Update(deltaTime, mapContainer); + foreach (GUITickBox tickBox in missionTickBoxes) + { + bool disable = hasMaxMissions && !tickBox.Selected; + tickBox.Enabled = Campaign.AllowedToManageCampaign() && !disable; + tickBox.Box.DisabledColor = disable ? tickBox.Box.Color * 0.5f : tickBox.Box.Color * 0.8f; + foreach (GUIComponent child in tickBox.Parent.Parent.Children) + { + if (child is GUITextBlock textBlock) + { + textBlock.SelectedTextColor = textBlock.HoverTextColor = textBlock.TextColor = + disable ? new Color(textBlock.TextColor, 0.5f) : new Color(textBlock.TextColor, 1.0f); + } + } + } } public void Update(float deltaTime) @@ -341,6 +367,7 @@ namespace Barotrauma public void SelectLocation(Location location, LocationConnection connection) { + missionTickBoxes.Clear(); locationInfoPanel.ClearChildren(); //don't select the map panel if we're looking at some other tab if (selectedTab == CampaignMode.InteractionType.Map) @@ -436,6 +463,19 @@ namespace Barotrauma { Spacing = (int)(5 * GUI.yScale) }; + missionList.OnSelected = (GUIComponent selected, object userdata) => + { + var tickBox = selected.FindChild(c => c is GUITickBox, recursive: true) as GUITickBox; + if (GUI.MouseOn == tickBox) { return false; } + if (tickBox != null) + { + if (Campaign.AllowedToManageCampaign() && tickBox.Enabled) + { + tickBox.Selected = !tickBox.Selected; + } + } + return true; + }; SelectedLevel = connection?.LevelData; Location currentDisplayLocation = Campaign.GetCurrentDisplayLocation(); @@ -444,9 +484,6 @@ namespace Barotrauma List availableMissions = currentDisplayLocation.GetMissionsInConnection(connection).ToList(); if (!availableMissions.Contains(null)) { availableMissions.Insert(0, null); } - Mission selectedMission = currentDisplayLocation.SelectedMission != null && availableMissions.Contains(currentDisplayLocation.SelectedMission) ? - currentDisplayLocation.SelectedMission : null; - missionList.Content.ClearChildren(); foreach (Mission mission in availableMissions) @@ -458,67 +495,93 @@ namespace Barotrauma var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionPanel.RectTransform, Anchor.Center)) { Stretch = true, - CanBeFocused = true + CanBeFocused = true, + AbsoluteSpacing = GUI.IntScale(5) }; var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUI.SubHeadingFont, wrap: true); + // missionName.RectTransform.MinSize = new Point(0, (int)(missionName.Rect.Height * 1.5f)); if (mission != null) - { - if (MapGenerationParams.Instance?.MissionIcon != 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) { - var icon = new GUIImage(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.X, 0) }, - MapGenerationParams.Instance.MissionIcon, scaleToFit: true) - { - Color = MapGenerationParams.Instance.IndicatorColor * 0.5f, - SelectedColor = MapGenerationParams.Instance.IndicatorColor, - HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f) - }; - icon.RectTransform.IsFixedSize = true; - - GUILayoutGroup difficultyIndicatorGroup = null; - if (mission.Difficulty.HasValue) - { - difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, - isHorizontal: true, childAnchor: Anchor.CenterRight) - { - AbsoluteSpacing = 1, - UserData = "difficulty" - }; - var difficultyColor = mission.GetDifficultyColor(); - for (int i = 0; i < mission.Difficulty; i++) - { - new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) - { - Color = difficultyColor * 0.5f, - SelectedColor = difficultyColor, - HoverColor = Color.Lerp(difficultyColor, Color.White, 0.5f) - }; - } - } + 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 = Campaign.AllowedToManageCampaign(); + tickBox.OnSelected += (GUITickBox tb) => + { + if (!Campaign.AllowedToManageCampaign()) { return false; } - float extraPadding = 0.5f * icon.Rect.Width; - float extraZPadding = difficultyIndicatorGroup != null ? mission.Difficulty.Value * (difficultyIndicatorGroup.Children.First().Rect.Width + difficultyIndicatorGroup.AbsoluteSpacing) : 0; - missionName.Padding = new Vector4(missionName.Padding.X + icon.Rect.Width + extraPadding, - missionName.Padding.Y, - missionName.Padding.Z + extraZPadding + extraPadding, - missionName.Padding.W); - missionName.CalculateHeightFromText(); + if (tb.Selected) + { + Campaign.Map.CurrentLocation.SelectMission(mission); + } + else + { + Campaign.Map.CurrentLocation.DeselectMission(mission); + } + + UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); + + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + Campaign.AllowedToManageCampaign()) + { + GameMain.Client?.SendCampaignState(); + } + return true; + }; + missionTickBoxes.Add(tickBox); + + GUILayoutGroup difficultyIndicatorGroup = null; + if (mission.Difficulty.HasValue) + { + difficultyIndicatorGroup = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, missionName.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest) { AbsoluteOffset = new Point((int)missionName.Padding.Z, 0) }, + isHorizontal: true, childAnchor: Anchor.CenterRight) + { + AbsoluteSpacing = 1, + UserData = "difficulty" + }; + var difficultyColor = mission.GetDifficultyColor(); + for (int i = 0; i < mission.Difficulty; i++) + { + new GUIImage(new RectTransform(Vector2.One, difficultyIndicatorGroup.RectTransform, scaleBasis: ScaleBasis.Smallest) { IsFixedSize = true }, "DifficultyIndicator", scaleToFit: true) + { + Color = difficultyColor, + SelectedColor = difficultyColor, + HoverColor = difficultyColor + }; + } } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(), wrap: true, parseRichText: true); + 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.Y, + missionName.Padding.Z + extraZPadding + extraPadding, + missionName.Padding.W); + missionName.CalculateHeightFromText(); + + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(10)) }, style: null); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(Submarine.MainSub), wrap: true, parseRichText: true); string reputationText = mission.GetReputationRewardText(mission.Locations[0]); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true, parseRichText: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true, parseRichText: true); } - missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(20)); + missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); foreach (GUIComponent child in missionTextContent.Children) { - var textBlock = child as GUITextBlock; - textBlock.Color = textBlock.SelectedColor = textBlock.HoverColor = Color.Transparent; - textBlock.HoverTextColor = textBlock.TextColor; - textBlock.TextColor *= 0.5f; + if (child is GUITextBlock textBlock) + { + textBlock.Color = textBlock.SelectedColor = textBlock.HoverColor = Color.Transparent; + textBlock.SelectedTextColor = textBlock.HoverTextColor = textBlock.TextColor; + } } missionPanel.OnAddedToGUIUpdateList = (c) => { @@ -537,36 +600,33 @@ namespace Barotrauma }; } } - missionList.Select(selectedMission); if (prevSelectedLocation == selectedLocation) { missionList.BarScroll = prevMissionListScroll; - } - - if (Campaign.AllowedToManageCampaign()) - { - missionList.OnSelected = (component, userdata) => - { - Mission mission = userdata as Mission; - if (Campaign.Map.CurrentLocation.SelectedMission == mission) { return false; } - Campaign.Map.CurrentLocation.SelectedMission = mission; - //RefreshMissionInfo(mission); - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - Campaign.AllowedToManageCampaign()) - { - GameMain.Client?.SendCampaignState(); - } - return true; - }; + missionList.UpdateDimensions(); + missionList.UpdateScrollBarSize(); } } + var destination = connection.OtherLocation(currentDisplayLocation); + UpdateMaxMissions(destination); - StartButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), content.RectTransform), + var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), isHorizontal: true); + + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), buttonArea.RectTransform), "", font: GUI.Style.SubHeadingFont) + { + TextGetter = () => + { + return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.MaxMissionCount}"); + } + }; + + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonArea.RectTransform), TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") { OnClicked = (GUIButton btn, object obj) => { - if (missionList.Content.Children.Any(c => c.UserData is Mission) && !(missionList.SelectedData is Mission)) + if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && + missionList.Content.Children.Any(c => c.UserData is Mission)) { var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => @@ -587,6 +647,8 @@ namespace Barotrauma Visible = Campaign.AllowedToEndRound() }; + buttonArea.RectTransform.MinSize = new Point(0, StartButton.RectTransform.MinSize.Y); + if (Level.Loaded != null && connection?.LevelData == Level.Loaded.LevelData && currentDisplayLocation == Campaign.Map?.CurrentLocation) @@ -594,6 +656,7 @@ namespace Barotrauma StartButton.Visible = false; missionList.Enabled = false; } + //locationInfoPanel?.UpdateAuto(1.0f); } public void SelectTab(CampaignMode.InteractionType tab) @@ -657,5 +720,10 @@ namespace Barotrauma { return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); } + + private void UpdateMaxMissions(Location location) + { + hasMaxMissions = Campaign.NumberOfMissionsAtLocation(location) >= Campaign.Settings.MaxMissionCount; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 6041af895..9f158fe81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -4731,7 +4731,7 @@ namespace Barotrauma.CharacterEditor rotation: 0, origin: orig, sourceRectangle: wearable.InheritSourceRect ? limb.ActiveSprite.SourceRect : wearable.Sprite.SourceRect, - scale: (wearable.InheritTextureScale ? 1 : 1 / RagdollParams.TextureScale) * spriteSheetZoom, + scale: (wearable.InheritTextureScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom, effects: SpriteEffects.None, color: Color.White, layerDepth: 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index d60265989..6d6d44024 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -305,7 +305,7 @@ namespace Barotrauma.CharacterEditor new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("ContentPackage")); var rightContainer = new GUIFrame(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), style: null); contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); - foreach (ContentPackage cp in ContentPackage.AllPackages) + foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) { #if !DEBUG if (cp == GameMain.VanillaContent) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs new file mode 100644 index 000000000..6da198254 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -0,0 +1,57 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class EditorScreen : Screen + { + public static Color BackgroundColor = GameSettings.SubEditorBackgroundColor; + + public void CreateBackgroundColorPicker() + { + var msgBox = new GUIMessageBox(TextManager.Get("CharacterEditor.EditBackgroundColor"), "", new[] { TextManager.Get("Reset"), TextManager.Get("OK")}, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + + var rgbLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + + // Generate R,G,B labels and parent elements + var layoutParents = new GUILayoutGroup[3]; + for (int i = 0; i < 3; i++) + { + var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + layoutParents[i] = colorContainer; + } + + // attach number inputs to our generated parent elements + var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.R }; + var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.G }; + var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = BackgroundColor.B }; + + rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; + rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; + + rInput.OnValueChanged = gInput.OnValueChanged = bInput.OnValueChanged = delegate + { + var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); + BackgroundColor = color; + GameSettings.SubEditorBackgroundColor = color; + }; + + // Reset button + msgBox.Buttons[0].OnClicked = (button, o) => + { + rInput.IntValue = 13; + gInput.IntValue = 37; + bInput.IntValue = 69; + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = (button, o) => + { + msgBox.Close(); + GameMain.Config.SaveNewPlayerConfig(); + return true; + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 1a891e878..ac7c804b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -227,7 +227,7 @@ namespace Barotrauma public static GUIMessageBox AskForConfirmation(string header, string body, Func onConfirm) { string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; - GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); // Cancel button msgBox.Buttons[1].OnClicked = delegate diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 636daf0f3..a179926ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -364,13 +364,15 @@ namespace Barotrauma Quad.Render(); } - float grainStrength = Character.Controlled?.GrainStrength ?? 0; - if (grainStrength > 0) + if (Character.Controlled is { } character) { + float grainStrength = character.GrainStrength; Rectangle screenRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: GrainEffect); - GUI.DrawRectangle(spriteBatch, screenRect, Color.White * grainStrength, isFilled: true); + GUI.DrawRectangle(spriteBatch, screenRect, Color.White, isFilled: true); GrainEffect.Parameters["seed"].SetValue(Rand.Range(0f, 1f, Rand.RandSync.Unsynced)); + GrainEffect.Parameters["intensity"].SetValue(grainStrength); + GrainEffect.Parameters["grainColor"].SetValue(character.GrainColor.ToVector4()); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0a990cf75..dcb4ae206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -469,18 +469,17 @@ namespace Barotrauma this.game = game; - menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + { + CanBeFocused = false + }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") + { + CanBeFocused = false + }; var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); - - new GUIButton(new RectTransform(new Vector2(0.1f, 0.05f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.BottomLeft) { RelativeOffset = new Vector2(0.25f, 0.02f) }, - TextManager.Get("Back"), style: "GUIButtonLarge") - { - OnClicked = SelectTab - }; - } #endregion @@ -867,9 +866,7 @@ namespace Barotrauma { int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers + (int)button.UserData, 1, NetConfig.MaxPlayers); - maxPlayersBox.Text = currMaxPlayers.ToString(); - return true; } @@ -1167,15 +1164,18 @@ namespace Barotrauma StartNewGame = StartGame }; - var startButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.BottomRight); + var startButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + RelativeSpacing = 0.05f + }; campaignSetupUI.StartButton.RectTransform.Parent = startButtonContainer.RectTransform; campaignSetupUI.StartButton.RectTransform.MinSize = new Point( (int)(campaignSetupUI.StartButton.TextBlock.TextSize.X * 1.5f), campaignSetupUI.StartButton.RectTransform.MinSize.Y); startButtonContainer.RectTransform.MinSize = new Point(0, campaignSetupUI.StartButton.RectTransform.MinSize.Y); - if (campaignSetupUI.EnableRadiationToggle != null) + if (campaignSetupUI.CampaignCustomizeButton != null) { - campaignSetupUI.EnableRadiationToggle.RectTransform.Parent = startButtonContainer.RectTransform; + campaignSetupUI.CampaignCustomizeButton.RectTransform.Parent = startButtonContainer.RectTransform; } campaignSetupUI.InitialMoneyText.RectTransform.Parent = startButtonContainer.RectTransform; } @@ -1322,8 +1322,18 @@ namespace Barotrauma }; maxPlayersBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), textAlignment: Alignment.Center) { - Text = maxPlayers.ToString(), - CanBeFocused = false + Text = maxPlayers.ToString() + }; + maxPlayersBox.OnEnterPressed += (GUITextBox sender, string text) => + { + maxPlayersBox.Deselect(); + return true; + }; + maxPlayersBox.OnDeselected += (GUITextBox sender, Microsoft.Xna.Framework.Input.Keys key) => + { + int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); + currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); + maxPlayersBox.Text = currMaxPlayers.ToString(); }; new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 151e4e858..7e9f8c97a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -45,6 +45,10 @@ namespace Barotrauma private readonly GUITickBox radiationEnabledTickBox; + private readonly GUIButton[] maxMissionCountButtons; + private readonly GUITextBlock maxMissionCountText; + private readonly GUITextBlock maxMissionCountDescription; + private readonly GUIButton[] traitorProbabilityButtons; private readonly GUITextBlock traitorProbabilityText; @@ -929,7 +933,7 @@ namespace Barotrauma } }; QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("pausemenusavequit"), textAlignment: Alignment.Center) + TextManager.Get("quitbutton"), textAlignment: Alignment.Center) { OnClicked = (_, __) => { @@ -969,6 +973,16 @@ namespace Barotrauma UserData = missionType, }; + if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) + { + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty) + { + UserData = (int)missionType, + Visible = false, + CanBeFocused = false + }; + continue; + } missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), TextManager.Get("MissionType." + missionType.ToString())) { @@ -1148,6 +1162,31 @@ namespace Barotrauma }; } + var maxMissionCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; + maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true) + { + ToolTip = TextManager.Get("maxmissioncounttooltip") + }; + var maxMissionCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), maxMissionCountSettingHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; + maxMissionCountButtons = new GUIButton[2]; + maxMissionCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleLeft") + { + OnClicked = (button, obj) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, maxMissionCount: -1); + return true; + } + }; + maxMissionCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), maxMissionCountContainer.RectTransform), "0", textAlignment: Alignment.Center, style: "GUITextBox"); + maxMissionCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), maxMissionCountContainer.RectTransform), style: "GUIButtonToggleRight") + { + OnClicked = (button, obj) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, maxMissionCount: 1); + return true; + } + }; + maxMissionCountSettingHolder.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); List settingsElements = settingsContent.Children.ToList(); for (int i = 0; i < settingsElements.Count; i++) @@ -1301,6 +1340,13 @@ namespace Barotrauma { radiationEnabledTickBox.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); } + maxMissionCountDescription.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + maxMissionCountText.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + foreach (var button in maxMissionCountButtons) + { + button.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + } + traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1322,7 +1368,7 @@ namespace Barotrauma roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); - ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted && SelectedMode != GameModePreset.MultiPlayerCampaign; + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; RefreshGameModeContent(); } @@ -3214,7 +3260,12 @@ namespace Barotrauma { for (int i = 0; i < missionTypeTickBoxes.Length; i++) { - MissionType missionType = (MissionType)((int)missionTypeTickBoxes[i].UserData); + MissionType missionType = (MissionType)(int)missionTypeTickBoxes[i].UserData; + if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) + { + missionTypeTickBoxes[i].Parent.Visible = false; + continue; + } if (SelectedMode == GameModePreset.Mission) { missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.CoOpMissionClasses.ContainsKey(missionType); @@ -3269,7 +3320,7 @@ namespace Barotrauma CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } - ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted && SelectedMode != GameModePreset.MultiPlayerCampaign; + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 6e70159f2..728752cca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Xml.Linq; using System.Text; using Barotrauma.Extensions; +using FarseerPhysics; #if DEBUG using System.IO; using System.Xml; @@ -15,57 +16,8 @@ using Barotrauma.IO; namespace Barotrauma { - class ParticleEditorScreen : Screen + class ParticleEditorScreen : EditorScreen { - class Emitter : ISerializableEntity - { - public float EmitTimer; - - public float BurstTimer; - - [Editable(), Serialize("0.0,360.0", false)] - public Vector2 AngleRange { get; private set; } - - [Editable(), Serialize("0.0,0.0", false)] - public Vector2 VelocityRange { get; private set; } - - [Editable(), Serialize("1.0,1.0", false)] - public Vector2 ScaleRange { get; private set; } - - [Editable(), Serialize(0, false)] - public int ParticleBurstAmount { get; private set; } - - [Editable(), Serialize(1.0f, false)] - public float ParticleBurstInterval { get; private set; } - - [Editable(), Serialize(1.0f, false)] - public float ParticlesPerSecond { get; private set; } - - public string Name - { - get - { - return TextManager.Get("particleeditor.emitter"); - } - } - - public Dictionary SerializableProperties - { - get; - private set; - } - - public Emitter() - { - ScaleRange = Vector2.One; - AngleRange = new Vector2(0.0f, 360.0f); - ParticleBurstAmount = 1; - ParticleBurstInterval = 1.0f; - - SerializableProperties = SerializableProperty.GetProperties(this); - } - } - private GUIComponent rightPanel, leftPanel; private GUIListBox prefabList; private GUITextBox filterBox; @@ -73,23 +25,38 @@ namespace Barotrauma private ParticlePrefab selectedPrefab; - private Emitter emitter; + private readonly ParticleEmitterProperties emitterProperties = new ParticleEmitterProperties(null) + { + ScaleMax = 1f, + ScaleMin = 1f, + AngleMax = 360f, + AngleMin = 0, + ParticlesPerSecond = 1f + }; + + private ParticleEmitterPrefab emitterPrefab; + private ParticleEmitter emitter; private readonly Camera cam; - public override Camera Cam - { - get - { - return cam; - } - } + public override Camera Cam => cam; + + private const string sizeRefFilePath = "Content/size_reference.png"; + private readonly Texture2D sizeReference; + private Vector2 sizeRefPosition = Vector2.Zero; + private readonly Vector2 sizeRefOrigin; + private bool sizeRefEnabled; public ParticleEditorScreen() { cam = new Camera(); GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); + if (File.Exists(sizeRefFilePath)) + { + sizeReference = TextureLoader.FromFile(sizeRefFilePath, compress: false); + sizeRefOrigin = new Vector2(sizeReference.Width / 2f, sizeReference.Height / 2f); + } } private void CreateUI() @@ -122,8 +89,8 @@ namespace Barotrauma } }; - var serializeToClipBoardButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.03f), paddedRightPanel.RectTransform), - TextManager.Get("editor.copytoclipboard")) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.03f), paddedRightPanel.RectTransform), + TextManager.Get("ParticleEditor.CopyPrefabToClipboard")) { OnClicked = (btn, obj) => { @@ -132,11 +99,18 @@ namespace Barotrauma } }; - emitter = new Emitter(); - var emitterEditorContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), paddedRightPanel.RectTransform), style: null); - var emitterEditor = new SerializableEntityEditor(emitterEditorContainer.RectTransform, emitter, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); - emitterEditor.RectTransform.RelativeSize = Vector2.One; - emitterEditorContainer.RectTransform.Resize(new Point(emitterEditorContainer.RectTransform.NonScaledSize.X, emitterEditor.ContentHeight), false); + new GUIButton(new RectTransform(new Vector2(1.0f, 0.03f), paddedRightPanel.RectTransform), + TextManager.Get("ParticleEditor.CopyEmitterToClipboard")) + { + OnClicked = (btn, obj) => + { + SerializeEmitterToClipboard(); + return true; + } + }; + + var emitterListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), paddedRightPanel.RectTransform)); + new SerializableEntityEditor(emitterListBox.Content.RectTransform, emitterProperties, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), paddedRightPanel.RectTransform)); @@ -157,7 +131,10 @@ namespace Barotrauma prefabList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), paddedLeftPanel.RectTransform)); prefabList.OnSelected += (GUIComponent component, object obj) => { + cam.Position = Vector2.Zero; selectedPrefab = obj as ParticlePrefab; + emitterPrefab = new ParticleEmitterPrefab(selectedPrefab, emitterProperties); + emitter = new ParticleEmitter(emitterPrefab); listBox.ClearChildren(); new SerializableEntityEditor(listBox.Content.RectTransform, selectedPrefab, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); //listBox.Content.RectTransform.NonScaledSize = particlePrefabEditor.RectTransform.NonScaledSize; @@ -198,19 +175,6 @@ namespace Barotrauma } } - private void Emit(Vector2 position) - { - float angle = MathHelper.ToRadians(Rand.Range(emitter.AngleRange.X, emitter.AngleRange.Y)); - Vector2 velocity = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Rand.Range(emitter.VelocityRange.X, emitter.VelocityRange.Y); - - var particle = GameMain.ParticleManager.CreateParticle(selectedPrefab, position, velocity, 0.0f); - - if (particle != null) - { - particle.Size *= Rand.Range(emitter.ScaleRange.X, emitter.ScaleRange.Y); - } - } - private void FilterEmitters(string text) { if (string.IsNullOrWhiteSpace(text)) @@ -263,9 +227,34 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; } + private void SerializeEmitterToClipboard() + { + XElement element = new XElement(nameof(ParticleEmitter)); + if (selectedPrefab is { } prefab) + { + element.Add(new XAttribute("particle", prefab.Identifier)); + } + + SerializableProperty.SerializeProperties(emitterProperties, element, saveIfDefault: false, ignoreEditable: true); + + StringBuilder sb = new StringBuilder(); + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + OmitXmlDeclaration = true + }; + + using (var writer = System.Xml.XmlWriter.Create(sb, settings)) + { + element.WriteTo(writer); + writer.Flush(); + } + + Clipboard.SetText(sb.ToString()); + } + private void SerializeToClipboard(ParticlePrefab prefab) { -#if WINDOWS if (prefab == null) { return; } System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings @@ -308,43 +297,39 @@ namespace Barotrauma } Clipboard.SetText(sb.ToString()); -#endif } public override void Update(double deltaTime) { cam.MoveCamera((float)deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); - - if (selectedPrefab != null) + + if (GUI.MouseOn is null && PlayerInput.PrimaryMouseButtonHeld()) { - emitter.EmitTimer += (float)deltaTime; - emitter.BurstTimer += (float)deltaTime; + sizeRefPosition = cam.ScreenToWorld(PlayerInput.MousePosition); + } + if (PlayerInput.SecondaryMouseButtonClicked()) + { + CreateContextMenu(); + } - if (emitter.ParticlesPerSecond > 0) - { - float emitInterval = 1.0f / emitter.ParticlesPerSecond; - while (emitter.EmitTimer > emitInterval) - { - Emit(Vector2.Zero); - emitter.EmitTimer -= emitInterval; - } - } - - if (emitter.BurstTimer > emitter.ParticleBurstInterval) - { - for (int i = 0; i < emitter.ParticleBurstAmount; i++) - { - Emit(Vector2.Zero); - } - emitter.BurstTimer = 0.0f; - } - + if (selectedPrefab != null && emitter != null) + { + emitter.Emit((float) deltaTime, Vector2.Zero); } GameMain.ParticleManager.Update((float)deltaTime); } + private void CreateContextMenu() + { + GUIContextMenu.CreateContextMenu + ( + new ContextMenuOption("subeditor.editbackgroundcolor", true, CreateBackgroundColorPicker), + new ContextMenuOption("editor.togglereferencecharacter", true, delegate { sizeRefEnabled = !sizeRefEnabled; }) + ); + } + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { cam.UpdateTransform(); @@ -357,7 +342,7 @@ namespace Barotrauma null, null, null, null, cam.Transform); - graphics.Clear(new Color(0.051f, 0.149f, 0.271f, 1.0f)); + graphics.Clear(BackgroundColor); GameMain.ParticleManager.Draw(spriteBatch, false, false, ParticleBlendState.AlphaBlend); GameMain.ParticleManager.Draw(spriteBatch, true, false, ParticleBlendState.AlphaBlend); @@ -374,6 +359,20 @@ namespace Barotrauma spriteBatch.End(); + if (sizeRefEnabled && !(sizeReference is null)) + { + spriteBatch.Begin(SpriteSortMode.Deferred, + BlendState.NonPremultiplied, + null, null, null, null, + cam.Transform); + + Vector2 pos = sizeRefPosition; + pos.Y = -pos.Y; + spriteBatch.Draw(sizeReference, pos, null, Color.White, 0f, sizeRefOrigin, new Vector2(0.4f), SpriteEffects.None, 0f); + + spriteBatch.End(); + } + //------------------------------------------------------- spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 666bb8471..a966ab977 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -66,6 +66,8 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + public virtual void OnFileDropped(string filePath, string extension) { } + public virtual void Release() { frame.RectTransform.Parent = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 29f734718..13936faa4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -363,7 +363,10 @@ namespace Barotrauma "VineSprite", "LeafSprite", "FlowerSprite", - "DecorativeSprite" + "DecorativeSprite", + "BarrelSprite", + "RailSprite", + "SchematicSprite" }; foreach (string spriteElementName in spriteElementNames) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 4cc90ee24..33ad81659 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -26,6 +26,8 @@ namespace Barotrauma //listbox that shows the files included in the item being created private GUIListBox createItemFileList; + private GUIImage previewIcon; + private System.IO.FileSystemWatcher createItemWatcher; private readonly List tabButtons = new List(); @@ -277,6 +279,24 @@ namespace Barotrauma SelectTab(Tab.Mods); } + public override void OnFileDropped(string filePath, string extension) + { + switch (extension) + { + case ".png": // workshop preview + case ".jpg": + case ".jpeg": + if (previewIcon == null || itemContentPackage == null) { break; } + + OnPreviewImageSelected(previewIcon, filePath); + break; + + default: + DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .png, .jpg or .jpeg)"); + break; + } + } + private void OnItemInstalled(ulong itemId) { RefreshSubscribedItems(); @@ -1263,7 +1283,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemPreviewImage"), font: GUI.SubHeadingFont); - var previewIcon = new GUIImage(new RectTransform(new Vector2(1.0f, 0.7f), topLeftColumn.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); + previewIcon = new GUIImage(new RectTransform(new Vector2(1.0f, 0.7f), topLeftColumn.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index d871d034e..92cc56d16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -19,7 +19,7 @@ using Barotrauma.IO; namespace Barotrauma { - class SubEditorScreen : Screen + class SubEditorScreen : EditorScreen { private static readonly string[] crewExperienceLevels = { @@ -177,8 +177,6 @@ namespace Barotrauma private Mode mode; - private Color backgroundColor = GameSettings.SubEditorBackgroundColor; - private Vector2 MeasurePositionStart = Vector2.Zero; // Prevent the mode from changing @@ -1007,6 +1005,12 @@ namespace Barotrauma string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) + { + frame.Color = Color.Magenta; + string colorStr = XMLExtensions.ColorToString(Color.MediumPurple); + frame.ToolTip += $"\n‖color:{colorStr}‖{ep.ContentPackage?.Name}‖color:end‖"; + } if (ep.HideInMenus) { frame.Color = Color.Red; @@ -1225,6 +1229,50 @@ namespace Barotrauma } } + public override void OnFileDropped(string filePath, string extension) + { + switch (extension) + { + case ".sub": // Submarine + SubmarineInfo info = new SubmarineInfo(filePath); + if (info.IsFileCorrupted) + { + DebugConsole.ThrowError($"Could not drag and drop the file. File \"{filePath}\" is corrupted!"); + info.Dispose(); + return; + } + + string body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name); + GUI.AskForConfirmation(TextManager.Get("Load"), body, onConfirm: () => LoadSub(info), onDeny: () => info.Dispose()); + break; + + case ".xml": // Item Assembly + string text = File.ReadAllText(filePath); + // PlayerInput.MousePosition doesn't update while the window is not active so we need to use this method + Vector2 mousePos = Mouse.GetState().Position.ToVector2(); + PasteAssembly(text, cam.ScreenToWorld(mousePos)); + break; + + case ".png": // submarine preview + case ".jpg": + case ".jpeg": + if (saveFrame == null) { break; } + + Texture2D texture = Sprite.LoadTexture(filePath); + previewImage.Sprite = new Sprite(texture, null, null); + if (Submarine.MainSub != null) + { + Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + } + + break; + + default: + DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .xml, .sub, .png or .jpg)"); + break; + } + } + /// /// Coroutine that waits 5 minutes and then runs itself recursively again to save the submarine into a temporary file /// @@ -1466,6 +1514,9 @@ namespace Barotrauma case SubmarineType.Wreck: contentType = ContentType.Wreck; break; + case SubmarineType.EnemySubmarine: + contentType = ContentType.EnemySubmarine; + break; } if (contentType != ContentType.Submarine) { @@ -1504,8 +1555,13 @@ namespace Barotrauma if (!string.IsNullOrEmpty(specialSavePath) && (string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(Submarine.MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(Submarine.MainSub?.Info.FilePath) != specialSavePath)) { + string submarineTypeTag = "SubmarineType." + Submarine.MainSub.Info.Type; + if (Submarine.MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) + { + submarineTypeTag = "MissionType.Pirate"; + } var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("savesubtospecialfolderprompt", - new string[] { "[type]", "[outpostpath]" }, new string[] { TextManager.Get("submarinetype." + Submarine.MainSub.Info.Type), specialSavePath }), + new string[] { "[type]", "[outpostpath]" }, new string[] { TextManager.Get(submarineTypeTag), specialSavePath }), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { @@ -1781,7 +1837,12 @@ namespace Barotrauma subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); foreach (SubmarineType subType in Enum.GetValues(typeof(SubmarineType))) { - subTypeDropdown.AddItem(TextManager.Get("submarinetype."+subType.ToString().ToLowerInvariant()), subType); + string textTag = "SubmarineType." + subType; + if (subType == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag)) + { + textTag = "MissionType.Pirate"; + } + subTypeDropdown.AddItem(TextManager.Get(textTag), subType); } //--------------------------------------- @@ -2020,30 +2081,27 @@ namespace Barotrauma Submarine.MainSub.Info.Price = Math.Max(Submarine.MainSub.Info.Price, basePrice); } - if (!Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) + var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { - var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) - { - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), classGroup.RectTransform), - TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); - GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); - classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); - classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); - classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); - classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); - classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); - classDropDown.OnSelected += (selected, userdata) => - { - SubmarineClass submarineClass = (SubmarineClass)userdata; - Submarine.MainSub.Info.SubmarineClass = submarineClass; - return true; - }; - - classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); - } + Stretch = true + }; + var classText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), classGroup.RectTransform), + TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true); + GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform)); + classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); + classDropDown.AddItem(TextManager.Get("submarineclass.undefined"), SubmarineClass.Undefined); + classDropDown.AddItem(TextManager.Get("submarineclass.scout"), SubmarineClass.Scout); + classDropDown.AddItem(TextManager.Get("submarineclass.attack"), SubmarineClass.Attack); + classDropDown.AddItem(TextManager.Get("submarineclass.transport"), SubmarineClass.Transport); + classDropDown.AddItem(TextManager.Get("submarineclass.deepdiver"), SubmarineClass.DeepDiver); + classDropDown.OnSelected += (selected, userdata) => + { + SubmarineClass submarineClass = (SubmarineClass)userdata; + Submarine.MainSub.Info.SubmarineClass = submarineClass; + return true; + }; + classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); + classText.Enabled = classDropDown.ButtonEnabled = !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2216,17 +2274,29 @@ namespace Barotrauma { Selected = Submarine.MainSub != null && Submarine.MainSub.Info.HasTag(tag), UserData = tag, - OnSelected = (GUITickBox tickBox) => { if (Submarine.MainSub == null) return false; + SubmarineTag tag = (SubmarineTag)tickBox.UserData; + if (tag == SubmarineTag.Shuttle) + { + if (tickBox.Selected) + { + classDropDown.SelectItem(SubmarineClass.Undefined); + } + else + { + classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); + } + classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; + } if (tickBox.Selected) { - Submarine.MainSub.Info.AddTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.AddTag(tag); } else { - Submarine.MainSub.Info.RemoveTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.RemoveTag(tag); } return true; } @@ -2305,7 +2375,6 @@ namespace Barotrauma if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } - private void CreateSaveAssemblyScreen() { SetMode(Mode.Default); @@ -2317,10 +2386,10 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.3f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 300) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.35f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 350) }); GUILayoutGroup paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { - AbsoluteSpacing = 5, + AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; @@ -2337,15 +2406,22 @@ namespace Barotrauma }; #endif - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), - TextManager.Get("SaveItemAssemblyDialogDescription")); - descriptionBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedSaveFrame.RectTransform)) + var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), paddedSaveFrame.RectTransform)); + descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.TopLeft), + font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { - UserData = "description", - Wrap = true, - Text = "" + Padding = new Vector4(10 * GUI.Scale) }; - + + descriptionBox.OnTextChanged += (textBox, text) => + { + Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText); + textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10)); + descriptionContainer.UpdateScrollBarSize(); + descriptionContainer.BarScroll = 1.0f; + return true; + }; + var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), paddedSaveFrame.RectTransform), style: null); new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel")) @@ -2361,6 +2437,7 @@ namespace Barotrauma { OnClicked = SaveAssembly }; + buttonArea.RectTransform.MinSize = new Point(0, buttonArea.Children.First().RectTransform.MinSize.Y); } /// @@ -2404,7 +2481,7 @@ namespace Barotrauma } } - bool hideInMenus = !(nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox) ? false : hideInMenusTickBox.Selected; + bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; #if DEBUG string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; #else @@ -2423,7 +2500,6 @@ namespace Barotrauma } #endif string filePath = Path.Combine(saveFolder, nameBox.Text + ".xml"); - if (File.Exists(filePath)) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); @@ -2438,18 +2514,27 @@ namespace Barotrauma } else { - Save(); + var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); + var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); + if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath) == ItemAssemblyPrefab.VanillaSaveFolder) + { + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); + } + else + { + Save(); + } } void Save() { - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList, nameBox.Text, descriptionBox.Text, hideInMenus)); + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); #if DEBUG doc.Save(filePath); #else doc.SaveSafe(filePath); #endif - new ItemAssemblyPrefab(filePath); + new ItemAssemblyPrefab(filePath, allowOverwrite: true); UpdateEntityList(); } @@ -2519,8 +2604,13 @@ namespace Barotrauma { if (prevSub == null || prevSub.Type != sub.Type) { + string textTag = "SubmarineType." + sub.Type; + if (sub.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag)) + { + textTag = "MissionType.Pirate"; + } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, - TextManager.Get("SubmarineType." + sub.Type), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") + TextManager.Get(textTag), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") { CanBeFocused = false }; @@ -2712,8 +2802,15 @@ namespace Barotrauma if (subList.SelectedComponent == null) { return false; } if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } + LoadSub(selectedSubInfo); + + return true; + } + + public void LoadSub(SubmarineInfo info) + { Submarine.Unload(); - var selectedSub = new Submarine(selectedSubInfo); + var selectedSub = new Submarine(info); Submarine.MainSub = selectedSub; Submarine.MainSub.UpdateTransform(interpolate: false); ClearUndoBuffer(); @@ -2744,8 +2841,6 @@ namespace Barotrauma }; adjustLightsPrompt.Buttons[1].OnClicked += adjustLightsPrompt.Close; } - - return true; } private void TryDeleteSub(SubmarineInfo sub) @@ -2822,7 +2917,7 @@ namespace Barotrauma } } - if (!string.IsNullOrEmpty(entityFilterBox.Text) || dummyCharacter?.SelectedConstruction?.OwnInventory != null) + if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } @@ -2835,7 +2930,7 @@ namespace Barotrauma private void FilterEntities(string filter) { - if (string.IsNullOrWhiteSpace(filter) && dummyCharacter?.SelectedConstruction?.OwnInventory == null) + if (string.IsNullOrWhiteSpace(filter)) { allEntityList.Visible = false; categorizedEntityList.Visible = true; @@ -2862,11 +2957,7 @@ namespace Barotrauma { child.Visible = (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && - ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); ; - if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) - { - child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); - } + ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); } allEntityList.UpdateScrollBarSize(); allEntityList.BarScroll = 0.0f; @@ -2942,11 +3033,14 @@ namespace Barotrauma new ContextMenuOption("SubEditor.EditBackgroundColor", isEnabled: true, onSelected: CreateBackgroundColorPicker), new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode), new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid), - new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, PasteAssembly), + new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, () => PasteAssembly()), new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate { - IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); - MapEntity.SelectedList.AddRange(matching); + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + { + if (MapEntity.SelectedList.Contains(match)) { continue; } + MapEntity.SelectedList.Add(match); + } }), new ContextMenuOption("SubEditor.AddImage", isEnabled: true, onSelected: ImageManager.CreateImageWizard), new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate @@ -2973,10 +3067,11 @@ namespace Barotrauma } } - private void PasteAssembly() + private void PasteAssembly(string text = null, Vector2? pos = null) { - string clipboard = Clipboard.GetText(); - if (string.IsNullOrWhiteSpace(clipboard)) + pos ??= cam.ScreenToWorld(PlayerInput.MousePosition); + text ??= Clipboard.GetText(); + if (string.IsNullOrWhiteSpace(text)) { DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is empty."); return; @@ -2986,7 +3081,7 @@ namespace Barotrauma try { - element = XDocument.Parse(clipboard).Root; + element = XDocument.Parse(text).Root; } catch (Exception) { /* ignored */ } @@ -2996,12 +3091,11 @@ namespace Barotrauma return; } - Vector2 pos = cam.ScreenToWorld(PlayerInput.MousePosition); Submarine sub = Submarine.MainSub; List entities; try { - entities = ItemAssemblyPrefab.PasteEntities(pos, sub, element, selectInstance: true); + entities = ItemAssemblyPrefab.PasteEntities(pos.Value, sub, element, selectInstance: true); } catch (Exception e) { @@ -3143,6 +3237,8 @@ namespace Barotrauma Color newColor = SetColor(null); + if (!IsSubEditor()) { return true; } + Dictionary> oldProperties = new Dictionary>(); foreach (var (sEntity, color, _) in entities) @@ -3274,57 +3370,6 @@ namespace Barotrauma static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; } - - /// - /// Creates a color picker that can be used to change the submarine editor's background color - /// - private void CreateBackgroundColorPicker() - { - var msgBox = new GUIMessageBox(TextManager.Get("CharacterEditor.EditBackgroundColor"), "", new[] { TextManager.Get("Reset"), TextManager.Get("OK")}, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); - - var rgbLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); - - // Generate R,G,B labels and parent elements - var layoutParents = new GUILayoutGroup[3]; - for (int i = 0; i < 3; i++) - { - var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); - layoutParents[i] = colorContainer; - } - - // attach number inputs to our generated parent elements - var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.R }; - var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.G }; - var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.B }; - - rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; - rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; - - rInput.OnValueChanged = gInput.OnValueChanged = bInput.OnValueChanged = delegate - { - var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); - backgroundColor = color; - GameSettings.SubEditorBackgroundColor = color; - }; - - // Reset button - msgBox.Buttons[0].OnClicked = (button, o) => - { - rInput.IntValue = 13; - gInput.IntValue = 37; - bInput.IntValue = 69; - return true; - }; - - // Ok button - msgBox.Buttons[1].OnClicked = (button, o) => - { - msgBox.Close(); - GameMain.Config.SaveNewPlayerConfig(); - return true; - }; - } private GUIFrame CreateWiringPanel() { @@ -3495,27 +3540,7 @@ namespace Barotrauma submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; } - - /// - /// Checks if the prefab is an item or if it only consists of items - /// - /// The prefab to check - /// True if the the prefab is an item or it contains only items - private bool IsItemPrefab(MapEntityPrefab mapPrefab) - { - if (dummyCharacter?.SelectedConstruction == null) - { - return false; - } - - return mapPrefab switch - { - ItemPrefab iPrefab => true, - ItemAssemblyPrefab aPrefab => aPrefab.DisplayEntities.All(pair => pair.First is ItemPrefab), - _ => false - }; - } - + private bool SelectPrefab(GUIComponent component, object obj) { allEntityList.Deselect(); @@ -3983,9 +4008,9 @@ namespace Barotrauma { loadFrame.AddToGUIUpdateList(); } - else if (saveFrame != null) + else { - saveFrame.AddToGUIUpdateList(); + saveFrame?.AddToGUIUpdateList(); } } @@ -4728,9 +4753,9 @@ namespace Barotrauma CloseItem(); } } - else if (MapEntity.SelectedList.Count == 1 && WiringMode) + else if (MapEntity.SelectedList.Count == 1 && WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item) { - (MapEntity.SelectedList[0] as Item)?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); + item.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } CharacterHUD.Update((float)deltaTime, dummyCharacter, cam); @@ -4753,7 +4778,7 @@ namespace Barotrauma sub.UpdateTransform(); } - graphics.Clear(backgroundColor); + graphics.Clear(BackgroundColor); ImageManager.Draw(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index dff1e31da..19a61d2a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -272,7 +272,9 @@ namespace Barotrauma } public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) - : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity) : SerializableProperty.GetProperties(entity), showName, style, elementHeight, titleFont) + : this(parent, entity, inGame ? + SerializableProperty.GetProperties(entity).Union(SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? false)) + : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont) { } @@ -447,6 +449,13 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); } + // Ensure that the values stay in sync (could be that we force the value in the property accessor). + bool propertyValue = (bool)property.GetValue(entity); + if (tickBox.Selected != propertyValue) + { + tickBox.Selected = propertyValue; + tickBox.Flash(Color.Red); + } return true; } }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 158de9924..d99335015 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -72,6 +72,8 @@ namespace Barotrauma private readonly static BackgroundMusic[] targetMusic = new BackgroundMusic[MaxMusicChannels]; private static List musicClips; + private static BackgroundMusic previousDefaultMusic; + private static float updateMusicTimer; //ambience @@ -824,22 +826,58 @@ namespace Barotrauma IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); + int mainTrackIndex = 0; if (suitableMusic.Count() == 0) { - targetMusic[0] = null; + targetMusic[mainTrackIndex] = null; } //switch the music if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[0] == null || currentMusic[0] == null || !suitableMusic.Any(m => m.File == currentMusic[0].Filename)) + else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m.File == currentMusic[mainTrackIndex].Filename)) { - targetMusic[0] = suitableMusic.GetRandom(); + if (currentMusicType == "default") + { + if (previousDefaultMusic == null) + { + targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandom(); + } + else + { + targetMusic[mainTrackIndex] = previousDefaultMusic; + } + } + else + { + targetMusic[mainTrackIndex] = suitableMusic.GetRandom(); + } } - + + + if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection) + { + // Find background noise loop for the current biome + IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? + GetSuitableMusicClips(Level.Loaded.LevelData?.Biome?.Identifier, currentIntensity) : + Enumerable.Empty(); + + int noiseLoopIndex = 1; + if (suitableNoiseLoops.Count() == 0) + { + targetMusic[noiseLoopIndex] = null; + } + // Switch the noise loop if nothing playing atm or the currently playing clip is not suitable anymore + else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m.File == currentMusic[noiseLoopIndex].Filename)) + { + targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandom(); + } + } + //get the appropriate intensity layers for current situation IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips("intensity", currentIntensity) : Enumerable.Empty(); - for (int i = 1; i < MaxMusicChannels; i++) + int intensityTrackStartIndex = 2; + for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m.File == targetMusic[i].File)) @@ -851,9 +889,9 @@ namespace Barotrauma foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing - if (targetMusic.Any(m => m != null && m.File == intensityMusic.File)) continue; + if (targetMusic.Any(m => m != null && m.File == intensityMusic.File)) { continue; } - for (int i = 1; i < MaxMusicChannels; i++) + for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { if (targetMusic[i] == null) { @@ -861,7 +899,7 @@ namespace Barotrauma break; } } - } + } updateMusicTimer = UpdateMusicInterval; } @@ -973,7 +1011,11 @@ namespace Barotrauma return "editor"; } - if (Screen.Selected != GameMain.GameScreen) { return firstTimeInMainMenu ? "menu" : "default"; } + if (Screen.Selected != GameMain.GameScreen) + { + previousDefaultMusic = null; + return firstTimeInMainMenu ? "menu" : "default"; + } firstTimeInMainMenu = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs index d1a282e15..42422aefa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs @@ -114,10 +114,7 @@ namespace Barotrauma.Sounds { lock (mutex) { - if (soundChannel != null) - { - soundChannel.Dispose(); - } + soundChannel?.Dispose(); base.Dispose(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 2a86615d1..143a70d25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -121,7 +121,7 @@ namespace Barotrauma } } - public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.Texture == texture) : new Sprite[] { this }); + public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.texture == texture).ToList() : new List() { this }); public void ReloadTexture(IEnumerable spritesToUpdate) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 7cd361e71..a65c33d7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -55,8 +55,6 @@ namespace Barotrauma partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition, bool playSound) { - if (entity == null) { return; } - if (playSound) { PlaySound(entity, hull, worldPosition); @@ -66,7 +64,7 @@ namespace Barotrauma { float angle = 0.0f; float particleRotation = 0.0f; - if (emitter.Prefab.CopyEntityAngle) + if (emitter.Prefab.Properties.CopyEntityAngle) { Limb targetLimb = null; if (entity is Item item && item.body != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index a930107d0..3ed3228ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -215,15 +215,10 @@ namespace Barotrauma } else { - DebugConsole.NewMessage($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})", Color.Orange); + DebugConsole.AddWarning($"Could not compress a texture because the dimensions aren't a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})"); } } - if (((width & 0x03) != 0) || ((height & 0x03) != 0)) - { - DebugConsole.AddWarning($"Cannot compress a texture because the dimensions are not a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})"); - } - Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index eb188b43c..3dade74cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -112,7 +112,7 @@ namespace Barotrauma GameMain.Instance.GraphicsDevice.Viewport = prevViewport; using (FileStream fs = File.Open("wikiimage.png", System.IO.FileMode.Create)) { - rt.SaveAsPng(fs, boundingBox.Width, boundingBox.Height); + rt.SaveAsPng(fs, texWidth, texHeight); } } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/grainshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/grainshader.xnb index 432e7191d..d795c8cac 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/grainshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/grainshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/grainshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/grainshader_opengl.xnb index dcf39677d..ebc56fc04 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/grainshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/grainshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 3a58ebb92..92e2f7fe6 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 9dcf56a03..337e40141 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/grainshader.fx b/Barotrauma/BarotraumaClient/Shaders/grainshader.fx index c5191f3a0..451684d1f 100644 --- a/Barotrauma/BarotraumaClient/Shaders/grainshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/grainshader.fx @@ -1,6 +1,7 @@ // vim:ft=hlsl -//float4 baseColor; float seed; +float intensity; +float4 grainColor; float nrand(float2 uv) { @@ -9,16 +10,15 @@ float nrand(float2 uv) float4 grain(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { - float4 baseColor = { 1, 1, 1, 0.25 }; + float4 baseColor = grainColor; float4 color = baseColor * nrand(texCoord); float2 center = { 0.5, 0.5 }; float2 diff = texCoord - center; float alpha = diff.x * diff.x + diff.y * diff.y; - color.a = alpha; - return clr * color; + color.a = alpha * intensity; + return color; } - technique Grain { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/Shaders/grainshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/grainshader_opengl.fx index bb9a45311..795b0e64b 100644 --- a/Barotrauma/BarotraumaClient/Shaders/grainshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/grainshader_opengl.fx @@ -1,6 +1,7 @@ // vim:ft=hlsl -//float4 baseColor; float seed; +float intensity; +float4 grainColor; float nrand(float2 uv) { @@ -9,16 +10,15 @@ float nrand(float2 uv) float4 grain(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { - float4 baseColor = { 1, 1, 1, 0.25 }; + float4 baseColor = grainColor; float4 color = baseColor * nrand(texCoord); float2 center = { 0.5, 0.5 }; float2 diff = texCoord - center; float alpha = diff.x * diff.x + diff.y * diff.y; - color.a = alpha; - return clr * color; + color.a = alpha * intensity; + return color; } - technique Grain { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ccd664f51..a5333c7b6 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 1f5d68a8f..bcbb4b32d 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 556643a5a..a73a20ca2 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 1e90df4f0..bcd25ddf9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -283,24 +283,25 @@ namespace Barotrauma if (extraData != null) { + const int min = 0, max = 9; switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 6); + msg.WriteRangedInteger(0, min, max); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, 0, 6); + msg.WriteRangedInteger(1, min, max); Client owner = (Client)extraData[1]; msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 6); + msg.WriteRangedInteger(2, min, max); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, 0, 6); + msg.WriteRangedInteger(3, min, max); if (Info?.Job == null) { msg.Write((byte)0); @@ -315,39 +316,83 @@ namespace Barotrauma } } break; + case NetEntityEvent.Type.SetAttackTarget: case NetEntityEvent.Type.ExecuteAttack: Limb attackLimb = extraData[1] as Limb; UInt16 targetEntityID = (UInt16)extraData[2]; int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; - msg.WriteRangedInteger(4, 0, 6); + msg.WriteRangedInteger(extraData[0] is NetEntityEvent.Type.SetAttackTarget ? 4 : 5, min, max); msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); msg.Write(targetEntityID); msg.Write((byte)targetLimbIndex); + msg.Write(extraData.Length > 4 ? (float)extraData[4] : 0); + msg.Write(extraData.Length > 5 ? (float)extraData[5] : 0); break; case NetEntityEvent.Type.AssignCampaignInteraction: - msg.WriteRangedInteger(5, 0, 6); + msg.WriteRangedInteger(6, min, max); msg.Write((byte)CampaignInteractionType); + msg.Write(RequireConsciousnessForCustomInteract); break; - case NetEntityEvent.Type.ObjectiveManagerOrderState: - msg.WriteRangedInteger(6, 0, 6); + case NetEntityEvent.Type.ObjectiveManagerState: + msg.WriteRangedInteger(7, min, max); + int type = (extraData[1] as string) switch + { + "order" => 1, + "objective" => 2, + _ => 0 + }; + msg.WriteRangedInteger(type, 0, 2); if (!(AIController is HumanAIController controller)) { msg.Write(false); break; } - var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); - if (!currentOrderInfo.HasValue) + if (type == 1) { - msg.Write(false); - break; + var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); + bool validOrder = currentOrderInfo.HasValue; + msg.Write(validOrder); + if (!validOrder) { break; } + var orderPrefab = currentOrderInfo.Value.Order.Prefab; + int orderIndex = Order.PrefabList.IndexOf(orderPrefab); + msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); + if (!orderPrefab.HasOptions) { break; } + int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Value.OrderOption); + if (optionIndex == -1) + { + DebugConsole.AddWarning($"Error while writing order data. Order option \"{(currentOrderInfo.Value.OrderOption ?? null)}\" not found in the order prefab \"{orderPrefab.Name}\"."); + } + msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); + } + else if (type == 2) + { + var objective = controller.ObjectiveManager.CurrentObjective; + bool validObjective = !string.IsNullOrEmpty(objective?.Identifier); + msg.Write(validObjective); + if (!validObjective) { break; } + msg.Write(objective.Identifier); + msg.Write(objective.Option ?? ""); + UInt16 targetEntityId = 0; + if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) + { + targetEntityId = operateObjective.OperateTarget.ID; + } + msg.Write(targetEntityId); + } + break; + case NetEntityEvent.Type.TeamChange: + msg.WriteRangedInteger(8, min, max); + msg.Write((byte)TeamID); + break; + case NetEntityEvent.Type.AddToCrew: + msg.WriteRangedInteger(9, min, max); + msg.Write((byte)(CharacterTeamType)extraData[1]); // team id + ushort[] inventoryItemIDs = (ushort[])extraData[2]; + msg.Write((ushort)inventoryItemIDs.Length); + for (int i = 0; i < inventoryItemIDs.Length; i++) + { + msg.Write(inventoryItemIDs[i]); } - msg.Write(true); - var orderPrefab = currentOrderInfo.Value.Order.Prefab; - int orderIndex = Order.PrefabList.IndexOf(orderPrefab); - msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); - if (!orderPrefab.HasOptions) { break; } - int optionIndex = orderPrefab.Options.IndexOf(currentOrderInfo.Value.OrderOption); - msg.WriteRangedInteger(optionIndex, 0, orderPrefab.Options.Length); break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 563e0195d..4e69294a8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1688,13 +1688,18 @@ namespace Barotrauma "heal", (Client client, Vector2 cursorWorldPos, string[] args) => { - Character healedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args); + bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase); + Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args); if (healedCharacter != null) { healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f); healedCharacter.Oxygen = 100.0f; healedCharacter.Bloodloss = 0.0f; healedCharacter.SetStun(0.0f, true); + if (healAll) + { + healedCharacter.CharacterHealth.RemoveAllAfflictions(); + } } } ); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 8b872694f..7569c73c0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -1,5 +1,7 @@ using Barotrauma.Networking; +using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -15,9 +17,45 @@ namespace Barotrauma private static readonly Dictionary lastActiveAction = new Dictionary(); private readonly HashSet targetClients = new HashSet(); + private readonly Dictionary ignoredClients = new Dictionary(); + public IEnumerable TargetClients { - get { return targetClients; } + get + { + UpdateIgnoredClients(); + return targetClients.Where(c => !ignoredClients.ContainsKey(c)); + } + } + + private void UpdateIgnoredClients() + { + if (ignoredClients.Any()) + { + HashSet clientsToRemove = null; + foreach (var k in ignoredClients.Keys) + { + if (ignoredClients[k] < DateTime.Now) + { + clientsToRemove ??= new HashSet(); + clientsToRemove.Add(k); + } + } + if (!(clientsToRemove is null)) + { + foreach (var k in clientsToRemove) + { + ignoredClients.Remove(k); + } + } + } + } + + public void IgnoreClient(Client c, float seconds) + { + if (!ignoredClients.ContainsKey(c)) { ignoredClients.Add(c, DateTime.Now); } + ignoredClients[c] = DateTime.Now + TimeSpan.FromSeconds(seconds); + Reset(); } private bool IsBlockedByAnotherConversation(IEnumerable targets) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs index 776cb20ed..7446dfd5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventManager.cs @@ -28,7 +28,14 @@ namespace Barotrauma continue; } - convAction.SelectedOption = selectedOption; + if (selectedOption == byte.MaxValue) + { + convAction.IgnoreClient(sender, 3f); + } + else + { + convAction.SelectedOption = selectedOption; + } return; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index 89446aa82..e649ec598 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,16 +1,20 @@ using Barotrauma.Networking; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class AbandonedOutpostMission : Mission { + private readonly List spawnedItems = new List(); + public override void ServerWriteInitial(IWriteMessage msg, Client c) { - if (characters.Count == 0) + msg.Write((ushort)spawnedItems.Count); + foreach (Item item in spawnedItems) { - throw new InvalidOperationException("Server attempted to write AbandonedOutpostMission data when no characters had been spawned."); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); } msg.Write((byte)characters.Count); @@ -22,7 +26,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 123f2e422..c7912eb2a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -21,7 +21,7 @@ namespace Barotrauma } } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (!initialized) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs new file mode 100644 index 000000000..5cb7f95bb --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -0,0 +1,30 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class EscortMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + if (characters.Count == 0) + { + throw new InvalidOperationException("Server attempted to write escort mission data when no characters had been spawned."); + } + + msg.Write((byte)characters.Count); + foreach (Character character in characters) + { + character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); + msg.Write(terroristCharacters.Contains(character)); + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index afbd78dbe..dbbc96902 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -6,10 +6,12 @@ namespace Barotrauma { partial void ShowMessageProjSpecific(int missionState) { - if (missionState >= Headers.Count && missionState >= Messages.Count) return; + int messageIndex = missionState - 1; + if (messageIndex >= Headers.Count && messageIndex >= Messages.Count) { return; } + if (messageIndex < 0) { return; } - string header = missionState < Headers.Count ? Headers[missionState] : ""; - string message = missionState < Messages.Count ? Messages[missionState] : ""; + string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; + string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs deleted file mode 100644 index 1c7c667af..000000000 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/OutpostDestroyMission.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Barotrauma.Networking; -using System.Collections.Generic; - -namespace Barotrauma -{ - partial class OutpostDestroyMission : AbandonedOutpostMission - { - private readonly List spawnedItems = new List(); - public override void ServerWriteInitial(IWriteMessage msg, Client c) - { - base.ServerWriteInitial(msg, c); - msg.Write((ushort)spawnedItems.Count); - foreach (Item item in spawnedItems) - { - item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..52b5687be --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -0,0 +1,30 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class PirateMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + // duplicate code from escortmission, should possibly be combined, though additional loot items might be added so maybe not + if (characters.Count == 0) + { + throw new InvalidOperationException("Server attempted to write escort mission data when no characters had been spawned."); + } + + msg.Write((byte)characters.Count); + foreach (Character character in characters) + { + character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 6abe50855..95b3ee358 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -35,6 +35,8 @@ namespace Barotrauma private set; } + public static Thread MainThread { get; private set; } + //only screens the server implements public static GameScreen GameScreen; public static NetLobbyScreen NetLobbyScreen; @@ -91,6 +93,8 @@ namespace Barotrauma Console.WriteLine("Initializing GameScreen"); GameScreen = new GameScreen(); + + MainThread = Thread.CurrentThread; } public void Init() @@ -388,6 +392,8 @@ namespace Barotrauma if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } if (GameSettings.SendUserStatistics) { GameAnalytics.OnQuit(); } + + MainThread = null; } public static void ResetFrameTime() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 4e8fee3f3..7f2316759 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -1,8 +1,7 @@ -using System; +using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.Networking; namespace Barotrauma { @@ -26,7 +25,6 @@ namespace Barotrauma /// /// Saves bots in multiplayer /// - /// public void SaveMultiplayer(XElement root) { XElement saveElement = new XElement("bots", new XAttribute("hasbots", HasBots)); @@ -40,8 +38,30 @@ namespace Barotrauma XElement characterElement = info.Save(saveElement); if (info.InventoryData != null) { characterElement.Add(info.InventoryData); } if (info.HealthData != null) { characterElement.Add(info.HealthData); } + if (info.OrderData != null) { characterElement.Add(info.OrderData); } } + SaveActiveOrders(saveElement); root.Add(saveElement); } + + public void ServerWriteActiveOrders(IWriteMessage msg) + { + ushort count = (ushort)ActiveOrders.Count(o => o.First != null && !o.Second.HasValue); + msg.Write(count); + if (count > 0) + { + foreach (var activeOrder in ActiveOrders) + { + if (!(activeOrder?.First is Order order) || activeOrder.Second.HasValue) { continue; } + OrderChatMessage.WriteOrder(msg, order, null, order.TargetSpatialEntity, null, 0, order.WallSectionIndex); + bool hasOrderGiver = order.OrderGiver != null; + msg.Write(hasOrderGiver); + if (hasOrderGiver) + { + msg.Write(order.OrderGiver.ID); + } + } + } + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 912f8c031..24523ad42 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -35,9 +35,14 @@ namespace Barotrauma character.SpawnInventoryItems(inventory, itemData); } - public void ApplyHealthData(CharacterInfo characterInfo, Character character) + public void ApplyHealthData(Character character) { - characterInfo.ApplyHealthData(character, healthData); + CharacterInfo.ApplyHealthData(character, healthData); } + + public void ApplyOrderData(Character character) + { + CharacterInfo.ApplyOrderData(character, OrderData); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index a4ee8f123..49abc7448 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -49,7 +49,15 @@ namespace Barotrauma { GameMain.NetLobbyScreen.ToggleCampaignMode(true); SaveUtil.LoadGame(selectedSave); - ((MultiPlayerCampaign)GameMain.GameSession.GameMode).LastSaveID++; + if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign) + { + mpCampaign.LastSaveID++; + } + else + { + DebugConsole.ThrowError("Unexpected game mode: " + GameMain.GameSession.GameMode); + return; + } DebugConsole.NewMessage("Campaign loaded!", Color.Cyan); DebugConsole.NewMessage( GameMain.GameSession.Map.SelectedLocation == null ? @@ -155,6 +163,64 @@ namespace Barotrauma } } + public void SaveInventories() + { + List prevCharacterData = new List(characterData); + //client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has a character) + characterData.RemoveAll(cd => cd.HasSpawned); + + //refresh the character data of clients who are still in the server + foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Character?.Info == null) { continue; } + if (c.Character.IsDead && c.Character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { continue; } + c.CharacterInfo = c.Character.Info; + characterData.RemoveAll(cd => cd.MatchesClient(c)); + characterData.Add(new CharacterCampaignData(c)); + } + + //refresh the character data of clients who aren't in the server anymore + foreach (CharacterCampaignData data in prevCharacterData) + { + if (data.HasSpawned && !characterData.Any(cd => cd.IsDuplicate(data))) + { + var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); + if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) + { + data.Refresh(character); + characterData.Add(data); + } + } + } + + characterData.ForEach(cd => cd.HasSpawned = false); + + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + + //remove all items that are in someone's inventory + foreach (Character c in Character.CharacterList) + { + if (c.Inventory == null) { continue; } + if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost) + { + Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInOutpost && it.OriginalModuleIndex > 0)); + } + + if (c.Info != null && c.IsBot) + { + if (c.IsDead && c.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { CrewManager.RemoveCharacterInfo(c.Info); } + c.Info.HealthData = new XElement("health"); + c.CharacterHealth.Save(c.Info.HealthData); + c.Info.InventoryData = new XElement("inventory"); + c.SaveInventory(); + c.Info.SaveOrderData(); + } + + c.Inventory.DeleteAllItems(); + } + } + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { lastUpdateID++; @@ -186,59 +252,7 @@ namespace Barotrauma if (success) { - List prevCharacterData = new List(characterData); - //client character has spawned this round -> remove old data (and replace with an up-to-date one if the client still has a character) - characterData.RemoveAll(cd => cd.HasSpawned); - - //refresh the character data of clients who are still in the server - foreach (Client c in GameMain.Server.ConnectedClients) - { - if (c.Character?.Info == null) { continue; } - if (c.Character.IsDead && c.Character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { continue; } - c.CharacterInfo = c.Character.Info; - characterData.RemoveAll(cd => cd.MatchesClient(c)); - characterData.Add(new CharacterCampaignData(c)); - } - - //refresh the character data of clients who aren't in the server anymore - foreach (CharacterCampaignData data in prevCharacterData) - { - if (data.HasSpawned && !characterData.Any(cd => cd.IsDuplicate(data))) - { - var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk); - if (character != null && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)) - { - data.Refresh(character); - characterData.Add(data); - } - } - } - - characterData.ForEach(cd => cd.HasSpawned = false); - - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); - - //remove all items that are in someone's inventory - foreach (Character c in Character.CharacterList) - { - if (c.Inventory == null) { continue; } - if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost) - { - Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInOutpost && it.OriginalModuleIndex > 0)); - } - - if (c.Info != null && c.IsBot) - { - if (c.IsDead && c.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { CrewManager.RemoveCharacterInfo(c.Info); } - c.Info.HealthData = new XElement("health"); - c.CharacterHealth.Save(c.Info.HealthData); - c.Info.InventoryData = new XElement("inventory"); - c.SaveInventory(c.Inventory, c.Info.InventoryData); - } - - c.Inventory.DeleteAllItems(); - } + SaveInventories(); yield return CoroutineStatus.Running; @@ -246,9 +260,13 @@ namespace Barotrauma { Submarine.MainSub = leavingSub; GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); foreach (Submarine sub in subsToLeaveBehind) { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } @@ -284,6 +302,8 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + CrewManager?.ClearCurrentOrders(); + //-------------------------------------- GameMain.Server.EndGame(transitionType); @@ -317,7 +337,7 @@ namespace Barotrauma CargoManager.OnSoldItemsChanged += () => { LastUpdateID++; }; UpgradeManager.OnUpgradesChanged += () => { LastUpdateID++; }; Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; }; - Map.OnMissionSelected += (loc, mission) => { LastUpdateID++; }; + Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; } //increment save ID so clients know they're lacking the most up-to-date save file @@ -416,8 +436,15 @@ namespace Barotrauma msg.Write(lastSaveID); msg.Write(map.Seed); msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); - msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); - msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex); + msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); + + var selectedMissionIndices = map.GetSelectedMissionIndices(); + msg.Write((byte)selectedMissionIndices.Count()); + foreach (int selectedMissionIndex in selectedMissionIndices) + { + msg.Write((byte)selectedMissionIndex); + } + msg.Write(map.AllowDebugTeleport); msg.Write(reputation != null); if (reputation != null) { msg.Write(reputation.Value); } @@ -497,6 +524,13 @@ namespace Barotrauma msg.Write((byte)level); } + msg.Write((ushort)UpgradeManager.PurchasedItemSwaps.Count); + foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) + { + msg.Write(itemSwap.ItemToRemove.ID); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + } + var characterData = GetClientCharacterData(c); if (characterData?.CharacterInfo == null) { @@ -513,7 +547,14 @@ namespace Barotrauma { UInt16 currentLocIndex = msg.ReadUInt16(); UInt16 selectedLocIndex = msg.ReadUInt16(); - byte selectedMissionIndex = msg.ReadByte(); + + byte selectedMissionCount = msg.ReadByte(); + List selectedMissionIndices = new List(); + for (int i = 0; i < selectedMissionCount; i++) + { + selectedMissionIndices.Add(msg.ReadByte()); + } + bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); @@ -563,6 +604,21 @@ namespace Barotrauma purchasedUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); } + ushort purchasedItemSwapCount = msg.ReadUInt16(); + List purchasedItemSwaps = new List(); + for (int i = 0; i < purchasedItemSwapCount; i++) + { + UInt16 itemToRemoveID = msg.ReadUInt16(); + Item itemToRemove = Entity.FindEntityByID(itemToRemoveID) as Item; + + string itemToInstallIdentifier = msg.ReadString(); + ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + + if (itemToRemove == null) { continue; } + + purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + } + if (!AllowedToManageCampaign(sender)) { DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign"); @@ -626,7 +682,9 @@ namespace Barotrauma Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } - if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndex); } + if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } + + CheckTooManyMissions(Map.CurrentLocation, sender); List currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); @@ -649,6 +707,26 @@ namespace Barotrauma int level = UpgradeManager.GetUpgradeLevel(prefab, category); GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); } + + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) + { + UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); + } + else + { + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + } + } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + UpgradeManager.CancelItemSwap(item); + item.PendingItemSwap = null; + } + } } public void ServerReadCrew(IReadMessage msg, Client sender) @@ -863,7 +941,7 @@ namespace Barotrauma CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); - UpgradeManager?.SavePendingUpgrades(modeElement, UpgradeManager?.PendingUpgrades); + UpgradeManager?.Save(modeElement); if (petsElement != null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/UpgradeManager.cs index 58ac829aa..949f18657 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/UpgradeManager.cs @@ -20,33 +20,5 @@ namespace Barotrauma } } } - - /// - /// Sends a message to all clients telling them that all upgrades on the submarine were reset. - /// - /// - /// is supposed to have a list of reloaded metadata but seeing as - /// this method is currently only used when switching submarines and that disables the repair NPC - /// until the next round so currently there's no need for it as we get the new values from the save - /// file anyways. - /// - /// - private void SendUpgradeResetMessage(Dictionary newUpgrades) - { - foreach (Client c in GameMain.Server.ConnectedClients) - { - IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte)ServerPacketHeader.RESET_UPGRADES); - outmsg.Write(true); - outmsg.Write(Campaign.Money); - // outmsg.Write((uint)newUpgrades.Count); - // foreach (var (key, value) in newUpgrades) - // { - // outmsg.Write(key); - // outmsg.Write((byte)value); - // } - GameMain.Server?.ServerPeer?.Send(outmsg, c.Connection, DeliveryMethod.Reliable); - } - } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 61f312772..b66f144ff 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Items.Components } //don't allow rewiring locked panels - if (Locked || !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return; } + if (Locked || TemporarilyLocked || !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return; } item.CreateServerEvent(this); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 609cc2e0b..6e583e89c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -8,11 +8,18 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { + private CoroutineHandle logPropertyChangeCoroutine; + public override Sprite Sprite { get { return prefab?.sprite; } } + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + } + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { string errorMsg = ""; @@ -86,6 +93,9 @@ namespace Barotrauma case NetEntityEvent.Type.Status: msg.Write(condition); break; + case NetEntityEvent.Type.AssignCampaignInteraction: + msg.Write((byte)CampaignInteractionType); + break; case NetEntityEvent.Type.Treatment: { ItemComponent targetComponent = (ItemComponent)extraData[1]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index dfcda4b3c..28f0cc043 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -22,80 +22,23 @@ namespace Barotrauma.Networking int? wallSectionIndex = null; if (type == ChatMessageType.Order) { - int orderIndex = msg.ReadByte(); - orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; - orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity; - - Order orderPrefab = null; - int? orderOptionIndex = null; - string orderOption = null; - - // The option of a Dismiss order is written differently so we know what order we target - // now that the game supports multiple current orders simultaneously - if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + var orderMessageInfo = OrderChatMessage.ReadOrder(msg); + if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) { - orderPrefab = Order.PrefabList[orderIndex]; - if (orderPrefab.Identifier != "dismissed") - { - orderOptionIndex = msg.ReadByte(); - } - // Does the dismiss order have a specified target? - else if(msg.ReadBoolean()) - { - int identifierCount = msg.ReadByte(); - if (identifierCount > 0) - { - int dismissedOrderIndex = msg.ReadByte(); - Order dismissedOrderPrefab = null; - if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) - { - dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; - orderOption = dismissedOrderPrefab.Identifier; - } - if (identifierCount > 1) - { - int dismissedOrderOptionIndex = msg.ReadByte(); - if (dismissedOrderPrefab != null) - { - var options = dismissedOrderPrefab.Options; - if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) - { - orderOption += $".{options[dismissedOrderOptionIndex]}"; - } - } - } - } - } - } - else - { - orderOptionIndex = msg.ReadByte(); - } - - int orderPriority = msg.ReadByte(); - orderTargetType = (Order.OrderTargetType)msg.ReadByte(); - if (msg.ReadBoolean()) - { - var x = msg.ReadSingle(); - var y = msg.ReadSingle(); - var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; - orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, true); - } - else if (orderTargetType == Order.OrderTargetType.WallSection) - { - wallSectionIndex = msg.ReadByte(); - } - - if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) - { - DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex})."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderMessageInfo.OrderIndex})."); if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } - - orderPrefab ??= Order.PrefabList[orderIndex]; - orderOption ??= orderOptionIndex == null || orderOptionIndex < 0 || orderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderOptionIndex.Value]; - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderPriority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) + orderTargetCharacter = orderMessageInfo.TargetCharacter; + orderTargetEntity = orderMessageInfo.TargetEntity; + orderTargetPosition = orderMessageInfo.TargetPosition; + orderTargetType = orderMessageInfo.TargetType; + wallSectionIndex = orderMessageInfo.WallSectionIndex; + var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; + string orderOption = orderMessageInfo.OrderOption ?? + (orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? + "" : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]); + orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) { WallSectionIndex = wallSectionIndex }; @@ -176,46 +119,49 @@ namespace Barotrauma.Networking if (type == ChatMessageType.Order) { if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; } - Order order = null; if (orderMsg.Order.IsReport) { HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); } - else if (orderTargetCharacter != null && !orderMsg.Order.TargetAllCharacters) + Order order = orderTargetType switch { - switch (orderTargetType) + Order.OrderTargetType.Entity => + new Order(orderMsg.Order, orderTargetEntity, orderMsg.Order?.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: orderMsg.Sender), + Order.OrderTargetType.Position => + new Order(orderMsg.Order, orderTargetPosition, orderGiver: orderMsg.Sender), + Order.OrderTargetType.WallSection when orderTargetEntity is Structure s && wallSectionIndex.HasValue => + new Order(orderMsg.Order, s, wallSectionIndex, orderGiver: orderMsg.Sender), + _ => throw new NotImplementedException() + }; + if (order != null) + { + if (order.TargetAllCharacters) { - case Order.OrderTargetType.Entity: - order = new Order(orderMsg.Order.Prefab, orderTargetEntity, orderMsg.Order.Prefab?.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: orderMsg.Sender); - break; - case Order.OrderTargetType.Position: - order = new Order(orderMsg.Order.Prefab, orderTargetPosition, orderGiver: orderMsg.Sender); - break; + if (order.IsIgnoreOrder) + { + switch (orderTargetType) + { + case Order.OrderTargetType.Entity: + if (!(orderTargetEntity is IIgnorable ignorableEntity)) { break; } + ignorableEntity.OrderedToBeIgnored = order.Identifier == "ignorethis"; + break; + case Order.OrderTargetType.Position: + throw new NotImplementedException(); + case Order.OrderTargetType.WallSection: + if (!wallSectionIndex.HasValue) { break; } + if (!(orderTargetEntity is Structure s)) { break; } + if (!(s.GetSection(wallSectionIndex.Value) is IIgnorable ignorableWall)) { break; } + ignorableWall.OrderedToBeIgnored = order.Identifier == "ignorethis"; + break; + } + } + GameMain.GameSession?.CrewManager?.AddOrder(order, order.IsIgnoreOrder ? (float?)null : order.FadeOutTime); } - if (order != null) + else if (orderTargetCharacter != null) { orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.OrderPriority, orderMsg.Sender); } } - else if (orderMsg.Order.IsIgnoreOrder) - { - switch (orderTargetType) - { - case Order.OrderTargetType.Entity: - if (orderTargetEntity is IIgnorable ignorableEntity) - { - ignorableEntity.OrderedToBeIgnored = orderMsg.Order.Identifier == "ignorethis"; - } - break; - case Order.OrderTargetType.WallSection: - if (!wallSectionIndex.HasValue) { break; } - if (orderTargetEntity is Structure s && s.GetSection(wallSectionIndex.Value) is IIgnorable ignorableWall) - { - ignorableWall.OrderedToBeIgnored = orderMsg.Order.Identifier == "ignorethis"; - } - break; - } - } GameMain.Server.SendOrderChatMessage(orderMsg); } else diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 0fa3ac16b..75273e01c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -378,7 +378,7 @@ namespace Barotrauma.Networking if (gameStarted) { - if (respawnManager != null) { respawnManager.Update(deltaTime); } + respawnManager?.Update(deltaTime); entityEventManager.Update(connectedClients); @@ -406,10 +406,7 @@ namespace Barotrauma.Networking } } - if (TraitorManager != null) - { - TraitorManager.Update(deltaTime); - } + TraitorManager?.Update(deltaTime); if (serverSettings.Voting.VoteRunning) { @@ -433,7 +430,7 @@ namespace Barotrauma.Networking connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); bool subAtLevelEnd = false; - if (Submarine.MainSub != null && Submarine.MainSubs[1] == null) + if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) { if (Level.Loaded?.EndOutpost != null) { @@ -488,8 +485,10 @@ namespace Barotrauma.Networking } else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode)) { +#if !DEBUG endRoundDelay = 1.0f; endRoundTimer += deltaTime; +#endif } else { @@ -537,7 +536,8 @@ namespace Barotrauma.Networking initiatedStartGame = false; } } - else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame) + else if (Screen.Selected == GameMain.NetLobbyScreen && !gameStarted && !initiatedStartGame && + (GameMain.NetLobbyScreen.SelectedMode != GameModePreset.MultiPlayerCampaign || GameMain.GameSession?.GameMode is MultiPlayerCampaign)) { if (serverSettings.AutoRestart) { @@ -1207,6 +1207,7 @@ namespace Barotrauma.Networking mpCampaign.ServerReadCrew(inc, sender); } } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); @@ -1315,6 +1316,12 @@ namespace Barotrauma.Networking if (gameStarted) { Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + if (mpCampaign != null && Level.IsLoadedOutpost) + { + mpCampaign.SaveInventories(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } EndGame(); } } @@ -1492,7 +1499,6 @@ namespace Barotrauma.Networking inc.ReadPadBits(); } - private void ClientWrite(Client c) { if (gameStarted && c.InGame) @@ -1893,6 +1899,7 @@ namespace Barotrauma.Networking } outmsg.Write(serverSettings.RadiationEnabled); + outmsg.Write((byte)serverSettings.MaxMissionCount); } else { @@ -2251,10 +2258,6 @@ namespace Barotrauma.Networking { client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } - else - { - client.CharacterInfo.ClearCurrentOrders(); - } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { @@ -2340,7 +2343,8 @@ namespace Barotrauma.Networking else { characterData.SpawnInventoryItems(spawnedCharacter, spawnedCharacter.Inventory); - characterData.ApplyHealthData(spawnedCharacter.Info, spawnedCharacter); + characterData.ApplyHealthData(spawnedCharacter); + characterData.ApplyOrderData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); characterData.HasSpawned = true; } @@ -2362,7 +2366,7 @@ namespace Barotrauma.Networking if (hadBots) { //loaded existing bots -> init them - crewManager?.InitRound(); + crewManager.InitRound(); } else { @@ -2372,6 +2376,7 @@ namespace Barotrauma.Networking } campaign?.LoadPets(); + crewManager?.LoadActiveOrders(); foreach (Submarine sub in Submarine.MainSubs) { @@ -2516,6 +2521,8 @@ namespace Barotrauma.Networking { mission.ServerWriteInitial(msg, client); } + msg.Write(GameMain.GameSession.CrewManager != null); + GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg); } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) @@ -3274,7 +3281,6 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub, true); } serverSettings.Voting.StopSubmarineVote(true); @@ -3314,7 +3320,6 @@ namespace Barotrauma.Networking serverSettings.SaveClientPermissions(); } - private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) { DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); @@ -3331,7 +3336,6 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private void SendClientPermissions(Client recipient, Client client) { if (recipient?.Connection == null) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 0e13b87f1..c6cc14e8d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -10,6 +10,11 @@ namespace Barotrauma.Networking { private DateTime despawnTime; + private float shuttleEmptyTimer; + + private int pendingRespawnCount, requiredRespawnCount; + private int prevPendingRespawnCount, prevRequiredRespawnCount; + private IEnumerable GetClientsToRespawn() { MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign; @@ -95,10 +100,19 @@ namespace Barotrauma.Networking RespawnShuttle.Velocity = Vector2.Zero; } - int clientsToRespawn = GetClientsToRespawn().Count(); + pendingRespawnCount = GetClientsToRespawn().Count(); + requiredRespawnCount = (int)Math.Max((float)GameMain.Server.ConnectedClients.Count * GameMain.Server.ServerSettings.MinRespawnRatio, 1.0f); + if (pendingRespawnCount != prevPendingRespawnCount || + requiredRespawnCount != prevRequiredRespawnCount) + { + prevPendingRespawnCount = pendingRespawnCount; + prevRequiredRespawnCount = requiredRespawnCount; + GameMain.Server.CreateEntityEvent(this); + } + if (RespawnCountdownStarted) { - if (clientsToRespawn == 0) + if (pendingRespawnCount == 0) { RespawnCountdownStarted = false; GameMain.Server.CreateEntityEvent(this); @@ -106,7 +120,7 @@ namespace Barotrauma.Networking } else { - bool shouldStartCountdown = ShouldStartRespawnCountdown(clientsToRespawn); + bool shouldStartCountdown = ShouldStartRespawnCountdown(pendingRespawnCount); if (shouldStartCountdown) { RespawnCountdownStarted = true; @@ -167,8 +181,14 @@ namespace Barotrauma.Networking } } - partial void UpdateReturningProjSpecific() + partial void UpdateReturningProjSpecific(float deltaTime) { + //speed up despawning if there's no-one inside the shuttle + if (despawnTime > DateTime.Now + new TimeSpan(0, 0, seconds: 30) && CheckShuttleEmpty(deltaTime)) + { + despawnTime = DateTime.Now + new TimeSpan(0, 0, seconds: 30); + } + foreach (Door door in shuttleDoors) { if (door.IsOpen) door.TrySetState(false, false, true); @@ -214,7 +234,7 @@ namespace Barotrauma.Networking if (!ReturnCountdownStarted) { //if there are no living chracters inside, transporting can be stopped immediately - if (!Character.CharacterList.Any(c => c.Submarine == RespawnShuttle && !c.IsDead)) + if (CheckShuttleEmpty(deltaTime)) { ReturnTime = DateTime.Now; ReturnCountdownStarted = true; @@ -232,6 +252,10 @@ namespace Barotrauma.Networking GameMain.Server.CreateEntityEvent(this); } } + else if (CheckShuttleEmpty(deltaTime)) + { + ReturnTime = DateTime.Now; + } if (DateTime.Now > ReturnTime) { @@ -245,6 +269,19 @@ namespace Barotrauma.Networking } } + private bool CheckShuttleEmpty(float deltaTime) + { + if (!Character.CharacterList.Any(c => c.Submarine == RespawnShuttle && !c.IsDead)) + { + shuttleEmptyTimer += deltaTime; + } + else + { + shuttleEmptyTimer = 0.0f; + } + return shuttleEmptyTimer > 1.0f; + } + partial void RespawnCharactersProjSpecific(Vector2? shuttlePos) { var respawnSub = RespawnShuttle ?? Submarine.MainSub; @@ -394,7 +431,7 @@ namespace Barotrauma.Networking else { characterData.SpawnInventoryItems(character, character.Inventory); - characterData.ApplyHealthData(character.Info, character); + characterData.ApplyHealthData(character); character.GiveIdCardTags(mainSubSpawnPoints[i]); characterData.HasSpawned = true; } @@ -427,6 +464,8 @@ namespace Barotrauma.Networking msg.Write((float)(ReturnTime - DateTime.Now).TotalSeconds); break; case State.Waiting: + msg.Write((ushort)pendingRespawnCount); + msg.Write((ushort)requiredRespawnCount); msg.Write(RespawnCountdownStarted); msg.Write((float)(RespawnTime - DateTime.Now).TotalSeconds); break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 1f2107de1..affe37a85 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -162,6 +162,11 @@ namespace Barotrauma.Networking RadiationEnabled = incMsg.ReadBoolean(); + int maxMissionCount = MaxMissionCount + incMsg.ReadByte() - 1; + if (maxMissionCount < CampaignSettings.MinMissionCountLimit) maxMissionCount = CampaignSettings.MaxMissionCountLimit; + if (maxMissionCount > CampaignSettings.MaxMissionCountLimit) maxMissionCount = CampaignSettings.MinMissionCountLimit; + MaxMissionCount = maxMissionCount; + changed |= true; } @@ -306,7 +311,8 @@ namespace Barotrauma.Networking { if (Enum.TryParse(missionTypeName, out MissionType missionType)) { - if (missionType == Barotrauma.MissionType.None) continue; + if (missionType == Barotrauma.MissionType.None) { continue; } + if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) { continue; } AllowedRandomMissionTypes.Add(missionType); } } @@ -323,6 +329,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); + GameMain.NetLobbyScreen.SetMaxMissionCount(MaxMissionCount); List monsterNames = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); MonsterEnabled = new Dictionary(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs index b7e826cb2..7c51dc64c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs @@ -37,6 +37,11 @@ namespace Barotrauma targetItems.Add(item); } } + //only target items in the main sub if there are any + if (targetItems.Count > 1 && targetItems.Any(it => it.Submarine == Submarine.MainSub)) + { + targetItems.RemoveAll(it => it.Submarine != Submarine.MainSub); + } if (targetItems.Count > 0) { var textId = targetItems[0].Prefab.GetItemNameTextId(); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4453b6107..a7f35504a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.13.3.11 + 0.14.6.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 47ab60408..19f21b7a6 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -38,10 +38,13 @@ + + + @@ -59,6 +62,7 @@ + @@ -105,6 +109,7 @@ + @@ -144,6 +149,7 @@ + @@ -252,4 +258,7 @@ + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs b/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs new file mode 100644 index 000000000..068a4d320 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/CachedDistance.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public class CachedDistance + { + public readonly Vector2 StartWorldPos; + public readonly Vector2 EndWorldPos; + public readonly float Distance; + public double RecalculationTime; + + public CachedDistance(Vector2 startWorldPos, Vector2 endWorldPos, float dist, double recalculationTime) + { + StartWorldPos = startWorldPos; + EndWorldPos = endWorldPos; + Distance = dist; + RecalculationTime = recalculationTime; + } + + public bool ShouldUpdateDistance(Vector2 currentStartWorldPos, Vector2 currentEndWorldPos, float minDistanceToUpdate = 500.0f) + { + if (Timing.TotalTime < RecalculationTime) { return false; } + float minDistSquared = minDistanceToUpdate * minDistanceToUpdate; + return Vector2.DistanceSquared(StartWorldPos, currentStartWorldPos) > minDistSquared || + Vector2.DistanceSquared(EndWorldPos, currentEndWorldPos) > minDistSquared; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs index 234ae434e..060108b4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs @@ -66,7 +66,7 @@ namespace Barotrauma private IEnumerable Update(ISpatialEntity targetEntity, Camera cam) { - if (targetEntity == null) { yield return CoroutineStatus.Success; } + if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; } prevControlled = Character.Controlled; if (RemoveControlFromCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index b4624d8ad..4129cb26d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -73,6 +73,8 @@ namespace Barotrauma get { return true; } } + public virtual bool IsMentallyUnstable => false; + private IEnumerable visibleHulls; private float hullVisibilityTimer; const float hullVisibilityInterval = 0.5f; @@ -215,10 +217,26 @@ namespace Barotrauma } private readonly HashSet unequippedItems = new HashSet(); - public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) + public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) { var pickable = item.GetComponent(); if (pickable == null) { return false; } + if (wear) + { + var wearable = item.GetComponent(); + if (wearable != null) + { + pickable = wearable; + } + } + else + { + var holdable = item.GetComponent(); + if (holdable != null) + { + pickable = holdable; + } + } if (item.ParentInventory is ItemInventory itemInventory) { if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } @@ -302,7 +320,7 @@ namespace Barotrauma { if (item != null && !item.Removed && Character.HasItem(item)) { - TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); + TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); } } unequippedItems.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 5de1618ac..7479ae3da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -144,21 +144,6 @@ namespace Barotrauma } } - public void Reset() - { - if (Static) - { - SightRange = MaxSightRange; - SoundRange = MaxSoundRange; - } - else - { - // Non-static ai targets must be kept alive by a custom logic (e.g. item components) - SightRange = StaticSight ? MaxSightRange : MinSightRange; - SoundRange = StaticSound ? MaxSoundRange : MinSoundRange; - } - } - public AITarget(Entity e, XElement element) : this(e) { SightRange = element.GetAttributeFloat("sightrange", 0.0f); @@ -242,5 +227,20 @@ namespace Barotrauma List.Remove(this); entity = null; } + + public void Reset() + { + if (Static) + { + SightRange = MaxSightRange; + SoundRange = MaxSoundRange; + } + else + { + // Non-static ai targets must be kept alive by a custom logic (e.g. item components) + SightRange = StaticSight ? MaxSightRange : MinSightRange; + SoundRange = StaticSound ? MaxSoundRange : MinSoundRange; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index dc73b621c..e63b0b3bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -16,6 +16,13 @@ namespace Barotrauma public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike } + public enum WallTargetingMethod + { + Target = 0x1, + Heading = 0x2, + Steering = 0x4 + } + partial class EnemyAIController : AIController { public static bool DisableEnemyAI; @@ -54,23 +61,27 @@ namespace Barotrauma private float attackLimbResetTimer; private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; + private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; private float FleeHealthThreshold => AIParams.FleeHealthThreshold; - private bool AggressiveBoarding => AIParams.AggressiveBoarding; + private bool IsAggressiveBoarder => AIParams.AggressiveBoarding; private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - //the limb selected for the current attack private Limb _attackingLimb; + private Limb _previousAttackingLimb; public Limb AttackingLimb { get { return _attackingLimb; } private set { attackLimbResetTimer = 0; + if (_attackingLimb != value) + { + _previousAttackingLimb = _attackingLimb; + } _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -342,6 +353,10 @@ namespace Barotrauma { targetingTag = "weaker"; } + else + { + targetingTag = "equal"; + } } } } @@ -381,6 +396,7 @@ namespace Barotrauma SelectedAiTarget = target; selectedTargetMemory = GetTargetMemory(target, true); selectedTargetMemory.Priority = priority; + ignoredTargets.Remove(target); } private float movementMargin; @@ -479,10 +495,6 @@ namespace Barotrauma { CharacterParams.TargetParams targetingParams = null; UpdateTargets(Character, out targetingParams); - if (!IsLatchedOnSub) - { - UpdateWallTarget(requiredHoleCount); - } updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) { @@ -493,10 +505,14 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } + if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive) + { + UpdateWallTarget(requiredHoleCount); + } } } - if (AIParams.Infiltrate) + if (AIParams.CanOpenDoors) { 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); @@ -787,7 +803,7 @@ namespace Barotrauma if (pathSteering != null && !Character.AnimController.InWater) { // Wander around inside - pathSteering.Wander(deltaTime, ConvertUnits.ToDisplayUnits(colliderLength), stayStillInTightSpace: false); + pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false); } else { @@ -1003,12 +1019,8 @@ namespace Barotrauma if (!w.SectionBodyDisabled(i)) { isBroken = false; - Vector2 sectionPos = w.SectionPosition(i); + Vector2 sectionPos = w.SectionPosition(i, world: true); attackWorldPos = sectionPos; - if (w.Submarine != null) - { - attackWorldPos += w.Submarine.Position; - } attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); break; } @@ -1026,18 +1038,19 @@ namespace Barotrauma bool pursue = false; if (IsCoolDownRunning) { - if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay) + var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; + if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - switch (AttackingLimb.attack.AfterAttack) + switch (currentAttackLimb.attack.AfterAttack) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; @@ -1050,13 +1063,13 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. UpdateFallBack(attackWorldPos, deltaTime, true); @@ -1067,7 +1080,7 @@ namespace Barotrauma else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1076,7 +1089,7 @@ namespace Barotrauma else { // No new limb was found. - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; @@ -1098,26 +1111,26 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1126,7 +1139,7 @@ namespace Barotrauma else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1134,13 +1147,13 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } break; case AIBehaviorAfterAttack.IdleUntilCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. UpdateIdle(deltaTime, followLastTarget: false); @@ -1148,7 +1161,7 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) @@ -1159,7 +1172,7 @@ namespace Barotrauma else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1203,7 +1216,7 @@ namespace Barotrauma } canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; } - if (!AIParams.Infiltrate) + if (!AIParams.CanOpenDoors) { if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) { @@ -1257,8 +1270,8 @@ namespace Barotrauma Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; - // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) - if (wallTarget != null) + // 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) { @@ -1282,9 +1295,14 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { - if (targetVelocity == Vector2.Zero) { return targetVelocity; } + if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } + float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); - return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot; + if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } + float distanceOffset = diff * AttackingLimb.attack.Duration; + // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. + return targetVelocity * distanceOffset * dot; } // Check that we can reach the target @@ -1422,10 +1440,11 @@ namespace Barotrauma else { pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true); - // Switch to Idle when cannot reach the target and if cannot damage the walls - if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) { State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); return; } } @@ -1648,7 +1667,7 @@ namespace Barotrauma float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } SteeringManager.SteeringSeek(steerPos, 10); - if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } @@ -1656,16 +1675,6 @@ namespace Barotrauma } if (canAttack) { - if (SelectedAiTarget.Entity is Item targetItem) - { - var door = targetItem.GetComponent(); - if (door != null && door.CanBeTraversed) - { - ResetAITarget(); - State = PreviousState; - return; - } - } if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); @@ -1777,6 +1786,7 @@ namespace Barotrauma } if (!isFriendly && attackResult.Damage > 0.0f) { + ignoredTargets.Remove(attacker.AiTarget); bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; if (AIParams.AttackWhenProvoked && canAttack) { @@ -1811,9 +1821,8 @@ namespace Barotrauma ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } - else + else if (!AIParams.HasTag("equal")) { - // Equal strength ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } @@ -1893,16 +1902,28 @@ namespace Barotrauma { //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] + { + Networking.NetEntityEvent.Type.SetAttackTarget, + attackingLimb, + (damageTarget as Entity)?.ID ?? Entity.NullEntityID, + damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, + SimPosition.X, + SimPosition.Y + }); +#endif if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { - if (damageTarget.Health > 0) + 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 selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed; } else { - selectedTargetMemory.Priority = 0; + selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); + return selectedTargetMemory.Priority > 1; } } return true; @@ -2138,6 +2159,10 @@ namespace Barotrauma { targetingTag = "weaker"; } + else + { + targetingTag = "equal"; + } if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) { if (SelectedAiTarget == aiTarget) @@ -2184,7 +2209,7 @@ namespace Barotrauma bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null; if (targetingFromOutsideToInside) { - if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls) + if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls) { // Can't reach continue; @@ -2258,25 +2283,24 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; - isInnerWall = isInnerWall || !leadsInside; if (Character.AnimController.CanEnterSubmarine) { if (!isCharacterInside) { if (CanPassThroughHole(s, i)) { - valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; + valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0; } - else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls) + else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls) { - // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside - valueModifier *= 1 + section.gap.Open; + // Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open * 0.25f; } } else { // Inside - if (AggressiveBoarding) + if (IsAggressiveBoarder) { if (!isInnerWall) { @@ -2293,6 +2317,10 @@ namespace Barotrauma valueModifier = 0; break; } + else + { + valueModifier = 0.1f; + } } else { @@ -2316,7 +2344,7 @@ namespace Barotrauma valueModifier = 0; break; } - else if (AggressiveBoarding) + else if (IsAggressiveBoarder) { // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside // (Bonethreshers) @@ -2350,17 +2378,24 @@ namespace Barotrauma // Ignore broken and open doors, if cannot enter submarine continue; } - if (AggressiveBoarding) + if (IsAggressiveBoarder) { - // Increase the priority if the character is outside and the door is from outside to inside if (character.CurrentHull == null) { - valueModifier *= isOpen ? 5 : 1; + // Increase the priority if the character is outside and the door is from outside to inside + if (door.CanBeTraversed) + { + valueModifier = 3; + } + else if (door.LinkedGap != null) + { + valueModifier = 1 + door.LinkedGap.Open; + } } else { // Inside -> ignore open doors and outer doors - valueModifier *= isOpen || isOutdoor ? 0 : 1; + valueModifier = isOpen || isOutdoor ? 0 : 1; } } } @@ -2605,91 +2640,165 @@ namespace Barotrauma } private WallTarget wallTarget; - + private readonly List<(Body, int, Vector2)> wallHits = new List<(Body, int, Vector2)>(3); private void UpdateWallTarget(int requiredHoleCount) { wallTarget = null; - if (State == AIState.Flee || State == AIState.Escape) { return; } - if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; } if (SelectedAiTarget == null) { return; } - if (SelectedAiTarget.Entity == null) { return; } - Vector2 rayStart = SimPosition; - Vector2 rayEnd = SelectedAiTarget.SimPosition; - if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + if (SelectedAiTarget.Entity == null) { return; } + if (HasValidPath(requireNonDirty: true)) { return; } + wallHits.Clear(); + Structure wall = null; + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { - rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; - } - else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) - { - rayEnd -= Character.Submarine.SimPosition; - } - Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); - if (Submarine.LastPickedFraction != 1.0f && closestBody != null) - { - if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts)) + Vector2 rayStart = SimPosition; + Vector2 rayEnd = SelectedAiTarget.SimPosition; + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { - int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); - float sectionDamage = wall.SectionDamage(sectionIndex); - for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++) + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading)) + { + Vector2 rayStart = SimPosition; + Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5); + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + { + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayStart -= Character.Submarine.SimPosition; + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering)) + { + Vector2 rayStart = SimPosition; + Vector2 rayEnd = rayStart + Steering * 5; + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + { + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayStart -= Character.Submarine.SimPosition; + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (wallHits.Any()) + { + Body closestBody = null; + float closestDistance = 0; + int sectionIndex = -1; + Vector2 sectionPos = Vector2.Zero; + foreach ((Body body, int index, Vector2 sectionPosition) in wallHits) + { + float distance = Vector2.DistanceSquared(SimPosition, sectionPosition); + if (closestBody == null || closestDistance == 0 || distance < closestDistance) { - if (wall.SectionBodyDisabled(i)) - { - if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) - { - sectionIndex = i; - break; - } - else - { - // Ignore and keep breaking other sections - continue; - } - } - if (wall.SectionDamage(i) > sectionDamage) - { - sectionIndex = i; - } + closestBody = body; + closestDistance = distance; + wall = closestBody.UserData as Structure; + sectionPos = sectionPosition; + sectionIndex = index; } - Vector2 sectionPos = wall.SectionPosition(sectionIndex); - Vector2 attachTargetNormal; - if (wall.IsHorizontal) + } + if (closestBody == null || sectionIndex == -1) { return; } + Vector2 attachTargetNormal; + if (wall.IsHorizontal) + { + attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y)); + sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y; + } + else + { + attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f); + sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; + } + LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); + if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) + { + if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) { - attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y)); - sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y; + bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent() != null; + // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces. + if (!isTargetingDoor) + { + ResetAITarget(); + } } else { - attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f); - sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; + wallTarget = new WallTarget(sectionPos, wall, sectionIndex); } - LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); - if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) + } + else + { + // Blocked by a disabled wall. + ResetAITarget(); + } + } + + void DoRayCast(Vector2 rayStart, Vector2 rayEnd) + { + Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); + if (hitTarget != null && IsValid(hitTarget, out wall)) + { + int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); + if (sectionIndex >= 0) { - if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine) + wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex))); + } + } + } + + Vector2 GetSectionPosition(Structure wall, int sectionIndex) + { + float sectionDamage = wall.SectionDamage(sectionIndex); + for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++) + { + if (wall.SectionBodyDisabled(i)) + { + if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) { - if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) - { - // Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces. - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - } - else - { - wallTarget = new WallTarget(sectionPos, wall, sectionIndex); - } + sectionIndex = i; + break; + } + else + { + // Ignore and keep breaking other sections + continue; } } - } - if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight) - { - if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine || - closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine) + if (wall.SectionDamage(i) > sectionDamage) { - // Cannot reach the target, because it's blocked by a disabled wall or a door - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); + sectionIndex = i; } } + return wall.SectionPosition(sectionIndex, world: false); + } + + bool IsValid(Body hit, out Structure wall) + { + wall = null; + if (Submarine.LastPickedFraction == 1.0f) { return false; } + if (!(hit.UserData is Structure w)) { return false; } + if (w.Submarine == null) { return false; } + if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } + if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; } + if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; } + wall = w; + return true; } } @@ -2698,7 +2807,7 @@ namespace Barotrauma if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount)) { WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex); - Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true); + Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world: true); return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime); } else if (SelectedAiTarget != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 4a2e438a9..e4d777e7f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -29,6 +29,9 @@ namespace Barotrauma private float flipTimer; private const float FlipInterval = 0.5f; + private float teamChangeTimer; + private const float TeamChangeInterval = 0.5f; + public const float HULL_SAFETY_THRESHOLD = 40; public const float HULL_LOW_OXYGEN_PERCENTAGE = 30; @@ -121,6 +124,33 @@ namespace Barotrauma } } + public MentalStateManager MentalStateManager { get; private set; } + + public void InitMentalStateManager() + { + if (MentalStateManager == null) + { + MentalStateManager = new MentalStateManager(Character, this); + } + MentalStateManager.Active = true; + } + + public override bool IsMentallyUnstable => + MentalStateManager == null ? false : + MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal && + MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused; + + public ShipCommandManager ShipCommandManager { get; private set; } + + public void InitShipCommandManager() + { + if (ShipCommandManager == null) + { + ShipCommandManager = new ShipCommandManager(Character); + } + ShipCommandManager.Active = true; + } + public HumanAIController(Character c) : base(c) { if (!c.IsHuman) @@ -204,9 +234,11 @@ namespace Barotrauma } } } - if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) + + if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) { // Spot enemies while staying outside or inside an enemy ship. + // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior enemycheckTimer -= deltaTime; if (enemycheckTimer < 0) { @@ -287,6 +319,8 @@ namespace Barotrauma } else { + Character.UpdateTeam(); + if (Character.CurrentHull != null) { if (Character.IsOnPlayerTeam) @@ -301,7 +335,7 @@ namespace Barotrauma } if (Character.SpeechImpediment < 100.0f) { - if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck) + if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) { ReportProblems(); } @@ -314,7 +348,7 @@ namespace Barotrauma if (objectiveManager.CurrentObjective == null) { return; } objectiveManager.DoCurrentObjective(deltaTime); - bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; + bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority; if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null) { if (Character.CurrentHull == null) @@ -395,6 +429,9 @@ namespace Barotrauma flipTimer = FlipInterval; } } + + MentalStateManager?.Update(deltaTime); + ShipCommandManager?.Update(deltaTime); } private void UnequipUnnecessaryItems() @@ -442,9 +479,8 @@ namespace Barotrauma Character.AnimController.InWater || Character.AnimController.HeadInWater || Character.CurrentHull == null || - Character.Submarine?.TeamID != Character.TeamID || + (Character.Submarine?.TeamID != Character.TeamID && !Character.IsEscorted) || // these instances should maybe be combined to a method ObjectiveManager.IsCurrentObjective() || - ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); if (oxygenLow && Character.CurrentHull.Oxygen > 0) { @@ -454,7 +490,7 @@ namespace Barotrauma { shouldKeepTheGearOn = true; } - bool removeDivingSuit = !shouldKeepTheGearOn; + bool removeDivingSuit = !shouldKeepTheGearOn && Character.Submarine?.TeamID == Character.TeamID && (!(ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo) || goTo.Target != Character); bool takeMaskOff = !shouldKeepTheGearOn; if (!shouldKeepTheGearOn && !oxygenLow) { @@ -505,7 +541,7 @@ namespace Barotrauma var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR); if (divingSuit != null) { - if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); HandleRelocation(divingSuit); @@ -550,7 +586,7 @@ namespace Barotrauma { if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) { - if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); HandleRelocation(mask); @@ -603,7 +639,7 @@ namespace Barotrauma Item item = Character.Inventory.GetItemInLimbSlot(hand); if (item == null) { continue; } - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID ) { findItemState = FindItemState.OtherItem; if (FindSuitableContainer(item, out Item targetContainer)) @@ -705,9 +741,10 @@ namespace Barotrauma suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i => { - if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } + if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } + if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { @@ -743,6 +780,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; + bool speak = true; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -759,6 +797,21 @@ namespace Barotrauma var orderPrefab = Order.GetPrefab("reportintruders"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; + if (target.IsEscorted) + { + if (!Character.IsPrisoner && target.IsPrisoner) + { + string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); + Character.Speak(msg, ChatMessageType.Order); + speak = false; + } + else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable) + { + string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); + Character.Speak(msg, ChatMessageType.Order); + speak = false; + } + } } } } @@ -771,7 +824,7 @@ namespace Barotrauma targetHull = hull; } } - if (IsBallastFloraNoticeable(Character, hull)) + if (IsBallastFloraNoticeable(Character, hull) && newOrder == null) { var orderPrefab = Order.GetPrefab("reportballastflora"); newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); @@ -824,20 +877,24 @@ namespace Barotrauma } } } - if (newOrder != null) + if (newOrder != null && speak) { - if (Character.TeamID == CharacterTeamType.FriendlyNPC) + // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented + if (!Character.IsEscorted) { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, - identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), - minDurationBetweenSimilar: 60.0f); - } - else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) - { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); + if (Character.TeamID == CharacterTeamType.FriendlyNPC) + { + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, + identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), + minDurationBetweenSimilar: 60.0f); + } + else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) + { + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); #endif + } } } } @@ -977,11 +1034,11 @@ namespace Barotrauma return; } float cumulativeDamage = GetDamageDoneByAttacker(attacker); - if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null) + bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; + if (isAccidental) { - if (cumulativeDamage > 1) + if (!Character.IsSecurity && cumulativeDamage > 1) { - // Don't retaliate on damage done by friendly NPC, because we know it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } @@ -1039,8 +1096,11 @@ namespace Barotrauma } else { - // Non-friendly - InformOtherNPCs(GetDamageDoneByAttacker(attacker)); + if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + { + // Non-friendly + InformOtherNPCs(GetDamageDoneByAttacker(attacker)); + } if (Character.IsBot) { AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); @@ -1051,7 +1111,7 @@ namespace Barotrauma { foreach (Character otherCharacter in Character.CharacterList) { - if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } + if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } if (otherCharacter.Submarine != Character.Submarine) { continue; } if (otherCharacter.Submarine != attacker.Submarine) { continue; } if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } @@ -1070,12 +1130,27 @@ namespace Barotrauma { if (!IsFriendly(attacker)) { - return c.AIController is HumanAIController humanAI && + if (Character.Submarine == null) + { + // Outside -> don't react. + return AIObjectiveCombat.CombatMode.None; + } + if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + { + // Attacked from an unconnected submarine. + return Character.SelectedConstruction?.GetComponent() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; + } + return c.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders)) ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive; } else { + if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + { + // Outside or attacked from an unconnected submarine -> don't react. + return AIObjectiveCombat.CombatMode.None; + } // If there are any enemies around, just ignore the friendly fire if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) { @@ -1090,7 +1165,7 @@ namespace Barotrauma // The guards don't react when the player attacks instigators. return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - else if (attacker.TeamID == CharacterTeamType.FriendlyNPC) + else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) { if (c.IsSecurity) { @@ -1132,7 +1207,7 @@ namespace Barotrauma } } - private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) + public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) { if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } @@ -1168,7 +1243,7 @@ namespace Barotrauma Character.Info?.Job?.Prefab.Identifier == "watchman" || Character.CurrentHull == null || Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective()?.Target is Character followTarget && followTarget.IsPlayer, - abortCondition = abortCondition, + AbortCondition = abortCondition, allowHoldFire = allowHoldFire, }; if (onAbort != null) @@ -1190,7 +1265,7 @@ namespace Barotrauma public void SetForcedOrder(Order order, string option, Character orderGiver) { - var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false); + var objective = ObjectiveManager.CreateObjective(order, option, orderGiver); ObjectiveManager.SetForcedOrder(objective); } @@ -1273,7 +1348,8 @@ namespace Barotrauma /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// - public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); + public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true, + predicate: (Item item) => { return character.HasEquippedItem(item, InvSlotType.OuterClothes); }); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. @@ -1401,7 +1477,9 @@ namespace Barotrauma Character thief = character; bool someoneSpoke = false; - if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) + bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false; + + if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1464,7 +1542,7 @@ namespace Barotrauma if (!humanAI.Character.IsSecurity) { return false; } if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(), - abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, + abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null, onAbort: () => { if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective()) @@ -1845,18 +1923,52 @@ namespace Barotrauma private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); - public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) + public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) { operatingCharacter = null; - if (character == null) { return false; } - if (target?.Item == null) { return false; } - bool isOrder = IsOrderedToOperateThis(character.AIController); - foreach (var c in Character.CharacterList) + float highestPriority = -1.0f; + float highestPriorityModifier = -1.0f; + foreach (Character c in Character.CharacterList) { - if (c == character) { continue; } - if (c.IsDead || c.IsIncapacitated) { continue; } - if (!IsFriendly(character, c, onlySameTeam: true)) { continue; } - operatingCharacter = c; + if (c.Removed) { continue; } + if (c.TeamID != team) { continue; } + if (c.IsIncapacitated) { continue; } + if (c.SelectedConstruction == target.Item) + { + operatingCharacter = c; + return true; + } + if (c.AIController is HumanAIController humanAI) + { + foreach (var objective in humanAI.ObjectiveManager.Objectives) + { + if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; } + if (operateObjective.Component.Item != target.Item) { continue; } + if (operateObjective.Priority < highestPriority) { continue; } + if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; } + operatingCharacter = c; + highestPriority = operateObjective.Priority; + highestPriorityModifier = operateObjective.PriorityModifier; + } + } + } + return operatingCharacter != null; + } + + // There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks. + // And even then there would be some differences that could end up being confusing (like the exception for steering). + public bool IsItemOperatedByAnother(ItemComponent target, out Character other) + { + other = null; + if (target?.Item == null) { return false; } + bool isOrder = IsOrderedToOperateThis(Character.AIController); + foreach (Character c in Character.CharacterList) + { + if (c == Character) { continue; } + if (c.Removed) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (c.IsIncapacitated) { continue; } + other = c; if (c.IsPlayer) { if (c.SelectedConstruction == target.Item) @@ -1887,7 +1999,7 @@ namespace Barotrauma } else { - if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective) + if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) { // The other bot is ordered to do something else continue; @@ -1895,12 +2007,12 @@ namespace Barotrauma if (target is Steering) { // Steering is hard-coded -> cannot use the required skills collection defined in the xml - if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) + if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) { return true; } } - else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c)) + else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c)) { return true; } @@ -1909,7 +2021,65 @@ namespace Barotrauma } } return false; - bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; + bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; + } + + public bool IsItemRepairedByAnother(Item target, out Character other) + { + other = null; + if (Character == null) { return false; } + if (target == null) { return false; } + bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); + foreach (var c in Character.CharacterList) + { + if (c == Character) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (c.IsIncapacitated) { continue; } + other = c; + if (c.IsPlayer) + { + if (target.Repairables.Any(r => r.CurrentFixer == c)) + { + // If the other character is player, don't try to repair + return true; + } + } + else if (c.AIController is HumanAIController operatingAI) + { + var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective(); + if (repairItemsObjective == null) { continue; } + if (repairItemsObjective.SubObjectives.None(o => o is AIObjectiveRepairItem repairObjective && repairObjective.Item == target)) + { + // Not targeting the same item. + continue; + } + bool isTargetOrdered = IsOrderedToRepairThis(operatingAI); + if (!isOrder && isTargetOrdered) + { + // If the other bot is ordered to repair the item, let him do it, unless we are ordered too + return true; + } + else + { + if (isOrder && !isTargetOrdered) + { + // We are ordered and the target is not -> allow to repair + continue; + } + else + { + if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) + { + // The other bot is ordered to do something else + continue; + } + return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c)); + } + } + } + } + return false; + bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target; } #region Wrappers @@ -1918,7 +2088,6 @@ namespace Barotrauma public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); - public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 022f6b756..cb8bdeff7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -301,34 +301,33 @@ namespace Barotrauma } Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; - if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item) + bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1); + if (useLadders && character.SelectedConstruction != ladders.Item) { - if (IsNextNodeLadder || currentPath.Finished) - { - if (character.CanInteractWith(ladders.Item)) - { - ladders.Item.TryInteract(character, false, true); - } - else - { - // Cannot interact with the current (or next) ladder, - // Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder. - // The intention of this code is to prevent the bots from dropping from the "double ladders". - var previousLadders = currentPath.PrevNode?.Ladders; - if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item && - character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5) - { - previousLadders.Item.TryInteract(character, false, true); - } - } - } - else if (!IsNextLadderSameAsCurrent && character.SelectedConstruction?.GetComponent() != null && character.CanInteractWith(ladders.Item)) + if (character.CanInteractWith(ladders.Item)) { ladders.Item.TryInteract(character, false, true); } + else + { + // Cannot interact with the current (or next) ladder, + // Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder. + // The intention of this code is to prevent the bots from dropping from the "double ladders". + var previousLadders = currentPath.PrevNode?.Ladders; + if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item && + character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5) + { + previousLadders.Item.TryInteract(character, false, true); + } + } } var collider = character.AnimController.Collider; - if (character.IsClimbing && !isDiving) + if (character.IsClimbing && !useLadders) + { + character.AnimController.Anim = AnimController.Animation.None; + character.SelectedConstruction = null; + } + if (character.IsClimbing && useLadders) { Vector2 diff = currentPath.CurrentNode.SimPosition - pos; bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; @@ -380,17 +379,12 @@ namespace Barotrauma } else if (character.AnimController.InWater) { - // If the character is underwater, we don't need the ladders anymore - if (character.IsClimbing && isDiving) - { - character.AnimController.Anim = AnimController.Animation.None; - character.SelectedConstruction = null; - } var door = currentPath.CurrentNode.ConnectedDoor; if (door == null || door.CanBeTraversed) { - float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - float targetDistance = collider.GetSize().X * multiplier; + float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); + Vector2 colliderSize = collider.GetSize(); + float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f); float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) @@ -404,24 +398,25 @@ namespace Barotrauma } } } - else if (!canClimb || !IsNextLadderSameAsCurrent) + else { // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderSize = collider.GetSize(); Vector2 velocity = collider.LinearVelocity; - // If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high. - float minHeight = 1; - // If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small. - float minWidth = 0.17f; + // If the character is very short, it would fail to use the waypoint nodes because they are always too high. + // If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small. + // Both values are based on the human size. So basically anything smaller than humans are considered as equal in size. + float minHeight = 1.6125001f; + float minWidth = 0.3225f; // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; var door = currentPath.CurrentNode.ConnectedDoor; - float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1)); - float targetDistance = Math.Max(collider.radius * margin, minWidth); + float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); + float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed)) { currentPath.SkipToNextNode(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs new file mode 100644 index 000000000..3f144bad8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -0,0 +1,176 @@ +using Barotrauma.Extensions; +using System; +using System.Linq; + +namespace Barotrauma +{ + partial class MentalStateManager + { + private float mentalStateTimer; + private const float MentalStateInterval = 7.5f; + + private float mentalBehaviorTimer; + private const float MentalBehaviorInterval = 7.5f; + + private readonly Character character; + private readonly HumanAIController humanAIController; + + public bool Active { get; set; } + public MentalType CurrentMentalType { get; private set; } + public enum MentalType + { + Normal, + Confused, // No effects other than special dialogue + Afraid, // Will retreat from whoever is nearby + Desperate, // Will defensively attack/arrest whoever is nearby + Berserk // turns fully hostile using team change logic + } + + private const string MentalTeamChange = "mental"; + + public MentalStateManager(Character character, HumanAIController humanAIController) + { + this.character = character; + this.humanAIController = humanAIController; + } + + public void Update(float deltaTime) + { + if (!Active) { return; } + mentalStateTimer -= deltaTime; + if (mentalStateTimer <= 0.0f) + { + UpdateMentalState(); + mentalStateTimer = MentalStateInterval * Rand.Range(0.75f, 1.25f); + } + + mentalBehaviorTimer = Math.Max(0f, mentalBehaviorTimer - deltaTime); + } + + private void UpdateMentalState() + { + MentalType newMentalType = GetMentalType(character.CharacterHealth.GetAffliction("psychosis")); + bool createdCombat = false; + + switch (newMentalType) + { + case MentalType.Normal: + case MentalType.Confused: + // remove combat if we became normal again + mentalBehaviorTimer = 0f; + break; + case MentalType.Afraid: + case MentalType.Desperate: + case MentalType.Berserk: + // berserk is not removed unless we drop to normal behavior again + if (CurrentMentalType == MentalType.Berserk) + { + newMentalType = MentalType.Berserk; + } + // give players a full interval to react to mental changes + if (newMentalType == CurrentMentalType) + { + createdCombat = CreateCombatBehavior(CurrentMentalType); + } + break; + } + + if (!createdCombat) + { + CreateDialogueBehavior(newMentalType); + } + + if (newMentalType != MentalType.Berserk) + { + character.TryRemoveTeamChange(MentalTeamChange); + } + + CurrentMentalType = newMentalType; + } + + private int mentalTypeCount; + private int MentalTypeCount + { + get + { + if (mentalTypeCount == 0) + { + mentalTypeCount = Enum.GetNames(typeof(MentalType)).Length; + } + return mentalTypeCount; + } + } + + private MentalType GetMentalType(Affliction affliction) + { + if (affliction == null) + { + 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 + { + 0 => MentalType.Normal, + 1 => MentalType.Confused, + 2 => MentalType.Afraid, + 3 => MentalType.Desperate, + 4 => MentalType.Berserk, + _ => throw new ArgumentOutOfRangeException(psychosisIndex.ToString()), + }; + return mentalType; + } + + public bool CreateCombatBehavior(MentalType mentalType) + { + Character mentalAttackTarget = Character.CharacterList.Where( + possibleTarget => HumanAIController.IsActive(possibleTarget) && + (possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) && + humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) && + possibleTarget != character).GetRandom(); + + if (mentalAttackTarget == null) + { + return false; + } + + var combatMode = AIObjectiveCombat.CombatMode.None; + bool holdFire = mentalType == MentalType.Afraid && character.IsSecurity; + switch (mentalType) + { + case MentalType.Afraid: + combatMode = character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat; + break; + case MentalType.Desperate: + // might be unnecessary to explicitly declare as arrest against non-humans + combatMode = character.IsSecurity && mentalAttackTarget.IsHuman ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Defensive; + break; + case MentalType.Berserk: + combatMode = AIObjectiveCombat.CombatMode.Offensive; + break; + } + + // using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that + mentalBehaviorTimer = MentalBehaviorInterval; + humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f); + string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}"; + character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f); + + if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange)) + { + // TODO: could this be handled in the switch block above? + character.TryAddNewTeamChange(MentalTeamChange, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Absolute, aggressiveBehavior: true)); + } + + return true; + } + + public void CreateDialogueBehavior(MentalType mentalType) + { + if (mentalType == MentalType.Normal) { return; } + string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}"; + character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 76b45c261..39f35025e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -64,6 +64,10 @@ namespace Barotrauma public readonly List Responses; private readonly int speakerIndex; private readonly List allowedSpeakerTags; + private readonly bool requireNextLine; + // used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere) + private readonly bool requireSight; + public static void LoadAll(IEnumerable files) { foreach (var file in files) @@ -161,6 +165,8 @@ namespace Barotrauma { Responses.Add(new NPCConversation(subElement, filePath)); } + requireNextLine = element.GetAttributeBool("requirenextline", false); + requireSight = element.GetAttributeBool("requiresight", false); } private static List GetCurrentFlags(Character speaker) @@ -211,7 +217,7 @@ namespace Barotrauma var afflictions = speaker.CharacterHealth.GetAllAfflictions(); foreach (Affliction affliction in afflictions) { - var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength); + var currentEffect = affliction.GetActiveEffect(); if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); @@ -226,7 +232,6 @@ namespace Barotrauma { currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType); } - if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)) { @@ -239,6 +244,10 @@ namespace Barotrauma currentFlags.Add("Hostage"); } } + if (speaker.IsEscorted) + { + currentFlags.Add("escort"); + } } return currentFlags; @@ -325,43 +334,15 @@ namespace Barotrauma foreach (Character potentialSpeaker in availableSpeakers) { - //check if the character has an appropriate job to say the line - if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || - selectedConversation.AllowedJobs.Count > 0) + if (CheckSpeakerViability(potentialSpeaker, selectedConversation, assignedSpeakers.Values.ToList(), ignoreFlags)) { - if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; } + allowedSpeakers.Add(potentialSpeaker); } - - //check if the character has all required flags to say the line - if (!ignoreFlags) - { - var characterFlags = GetCurrentFlags(potentialSpeaker); - if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { continue; } - } - - //check if the character is close enough to hear the rest of the speakers - if (assignedSpeakers.Values.Any(s => !potentialSpeaker.CanHearCharacter(s))) { continue; } - - //check if the character has an appropriate personality - if (selectedConversation.allowedSpeakerTags.Count > 0) - { - if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; } - if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; } - } - else - { - if (potentialSpeaker.Info?.PersonalityTrait != null && - !potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none")) - { - continue; - } - } - - allowedSpeakers.Add(potentialSpeaker); } - if (allowedSpeakers.Count == 0) + if (allowedSpeakers.Count == 0 || NextLineFailure(selectedConversation, availableSpeakers, allowedSpeakers, ignoreFlags)) { + allowedSpeakers.Clear(); potentialLines.Remove(selectedConversation); } else @@ -385,6 +366,62 @@ namespace Barotrauma CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations); } + static bool NextLineFailure(NPCConversation selectedConversation, List availableSpeakers, List allowedSpeakers, bool ignoreFlags) + { + if (selectedConversation.requireNextLine) + { + foreach (NPCConversation nextConversation in selectedConversation.Responses) + { + foreach (Character potentialNextSpeaker in availableSpeakers) + { + if (CheckSpeakerViability(potentialNextSpeaker, nextConversation, allowedSpeakers, ignoreFlags)) + { + return false; + } + } + } + return true; + } + return false; + } + + static bool CheckSpeakerViability(Character potentialSpeaker, NPCConversation selectedConversation, List checkedSpeakers, bool ignoreFlags) + { + //check if the character has an appropriate job to say the line + if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0) + { + if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; } + } + + //check if the character has all required flags to say the line + if (!ignoreFlags) + { + var characterFlags = GetCurrentFlags(potentialSpeaker); + if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { return false; } + } + + //check if the character is close enough to hear the rest of the speakers + if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; } + + //check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method) + if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; } + + //check if the character has an appropriate personality + if (selectedConversation.allowedSpeakerTags.Count > 0) + { + if (potentialSpeaker.Info?.PersonalityTrait == null) { return false; } + if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { return false; } + } + else + { + if (potentialSpeaker.Info?.PersonalityTrait != null && + !potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none")) + { + return false; + } + } + return true; + } private static NPCConversation GetRandomConversation(List conversations, bool avoidPreviouslyUsed) { if (!avoidPreviouslyUsed) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 23a43a38a..66376cc1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -6,16 +6,18 @@ using Barotrauma.Extensions; namespace Barotrauma { - abstract class AIObjective + abstract partial class AIObjective { public virtual float Devotion => AIObjectiveManager.baseDevotion; - public abstract string DebugTag { get; } + public abstract string Identifier { get; set; } + public virtual string DebugTag => Identifier; public virtual bool ForceRun => false; public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; public virtual bool AllowSubObjectiveSorting => false; public virtual bool ForceOrderPriority => true; + public virtual bool PrioritizeIfSubObjectivesActive => false; /// /// Can there be multiple objective instaces of the same type? @@ -52,8 +54,32 @@ namespace Barotrauma /// public float Priority { get; set; } public float BasePriority { get; set; } - public float PriorityModifier { get; private set; } = 1; + + private float resetPriorityTimer; + private readonly float resetPriorityTime = 1; + private bool _forceHighestPriority; + // For forcing the highest priority temporarily. Will reset automatically after one second, unless kept alive by something. + public bool ForceHighestPriority + { + get { return _forceHighestPriority; } + set + { + if (_forceHighestPriority == value) { return; } + _forceHighestPriority = value; + if (_forceHighestPriority) + { + resetPriorityTimer = resetPriorityTime; + } + } + } + + // For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something. + // The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority. + public bool ForceWalk { get; set; } + + public bool IgnoreAtOutpost { get; set; } + public readonly Character character; public readonly AIObjectiveManager objectiveManager; public string Option { get; private set; } @@ -102,6 +128,13 @@ namespace Barotrauma return all; } +#pragma warning disable CS0649 + /// + /// Aborts the objective when this condition is true. + /// + public Func AbortCondition; +#pragma warning restore CS0649 + /// /// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior. /// @@ -153,7 +186,6 @@ namespace Barotrauma Act(deltaTime); } - // TODO: check turret aioperate public void AddSubObjective(AIObjective objective, bool addFirst = false) { var type = objective.GetType(); @@ -217,18 +249,22 @@ namespace Barotrauma protected bool IsAllowed { get - { + { + if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + { + if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) + { + return false; + } + } if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } if (AllowInAnySub) { return true; } - if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { 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); } } - /// - /// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate. - /// - public virtual float GetPriority() + protected virtual float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) @@ -248,6 +284,17 @@ namespace Barotrauma return Priority; } + /// + /// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate. + /// + public float CalculatePriority() + { + Priority = GetPriority(); + ForceHighestPriority = false; + ForceWalk = false; + return Priority; + } + private void UpdateDevotion(float deltaTime) { var currentObjective = objectiveManager.CurrentObjective; @@ -261,6 +308,14 @@ namespace Barotrauma public virtual void Update(float deltaTime) { + if (resetPriorityTimer > 0) + { + resetPriorityTimer -= deltaTime; + } + else + { + ForceHighestPriority = false; + } if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0) { UpdateDevotion(deltaTime); @@ -393,7 +448,17 @@ namespace Barotrauma } } - protected abstract bool Check(); + protected virtual bool Check() + { + if (AbortCondition != null && AbortCondition(this)) + { + Abandon = true; + return false; + } + return CheckObjectiveSpecific(); + } + + protected abstract bool CheckObjectiveSpecific(); private bool CheckState() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index 1835b0a4f..ae6eb6ead 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveChargeBatteries : AIObjectiveLoop { - public override string DebugTag => "charge batteries"; + public override string Identifier { get; set; } = "charge batteries"; public override bool AllowAutomaticItemUnequipping => true; private IEnumerable batteryList; @@ -20,7 +20,7 @@ namespace Barotrauma { if (battery == null) { return false; } var item = battery.Item; - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 6b99d3899..e2ca2f781 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveCleanupItem : AIObjective { - public override string DebugTag => "cleanup item"; + public override string Identifier { get; set; } = "cleanup item"; public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; @@ -26,7 +26,7 @@ namespace Barotrauma this.item = item; } - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { @@ -61,14 +61,14 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (item.IgnoreByAI) + if (item.IgnoreByAI(character)) { Abandon = true; return; } if (item.ParentInventory != null) { - if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders())) + if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) { // Target was picked up or moved by someone. Abandon = true; @@ -82,14 +82,14 @@ namespace Barotrauma itemIndex = 0; if (suitableContainer != null) { - bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || ( - item.GetComponent() == null && + bool equip = item.GetComponent() != null || item.AllowedSlots.None(s => - s == InvSlotType.Card || - s == InvSlotType.Head || - s == InvSlotType.Headset || - s == InvSlotType.InnerClothes || - s == InvSlotType.OuterClothes)); + s == InvSlotType.Card || + s == InvSlotType.Head || + s == InvSlotType.Headset || + s == InvSlotType.InnerClothes || + s == InvSlotType.OuterClothes); + TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { Equip = equip, @@ -131,7 +131,7 @@ namespace Barotrauma } } - protected override bool Check() => IsCompleted; + protected override bool CheckObjectiveSpecific() => IsCompleted; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index cb1e3f299..f0cf653a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveCleanupItems : AIObjectiveLoop { - public override string DebugTag => "cleanup items"; + public override string Identifier { get; set; } = "cleanup items"; public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; public override bool ForceOrderPriority => false; @@ -38,8 +38,8 @@ namespace Barotrauma float prio = objectiveManager.GetOrderPriority(this); if (subObjectives.All(so => so.SubObjectives.None())) { - // If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold. - prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1); + // If none of the subobjectives have subobjectives, no valid container was found. Don't allow running. + ForceWalk = true; } return prio; } @@ -80,18 +80,29 @@ namespace Barotrauma return true; } - public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) => - !item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character); + public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => + allowUnloading && + !container.IgnoreByAI(character) && + container.IsInteractable(character) && + container.HasTag("allowcleanup") && + container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && + container.GetComponent() is ItemContainer itemContainer && itemContainer.HasAccess(character) && + IsItemInsideValidSubmarine(container, character); public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.SpawnedInOutpost) { return false; } if (item.ParentInventory != null) { - if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; } + if (item.Container == null) + { + // In a character inventory + return false; + } + if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } var pickable = item.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index b85e9ca21..fa7112563 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveCombat : AIObjective { - public override string DebugTag => "combat"; + public override string Identifier { get; set; } = "combat"; public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -92,11 +92,6 @@ namespace Barotrauma private readonly float distanceCheckInterval = 0.2f; private float distanceTimer; - /// - /// Aborts the objective when this condition is true - /// - public Func abortCondition; - public bool allowHoldFire; /// @@ -152,7 +147,7 @@ namespace Barotrauma HumanAIController.SortTimer = 0; } - public override float GetPriority() + protected override float GetPriority() { if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null) { @@ -186,7 +181,7 @@ namespace Barotrauma { findSafety.Priority = 0; } - if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective()) + if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective()) { distanceTimer -= deltaTime; if (distanceTimer < 0) @@ -197,7 +192,7 @@ namespace Barotrauma } } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { if (sqrDistance > maxDistance * maxDistance) { @@ -209,11 +204,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (abortCondition != null && abortCondition()) - { - Abandon = true; - return; - } if (AllowCoolDown) { coolDownTimer -= deltaTime; @@ -358,6 +348,7 @@ namespace Barotrauma TryAddSubObjective(ref seekWeaponObjective, constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false) { + AllowStealing = HumanAIController.IsMentallyUnstable, GetItemPriority = i => { if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; } @@ -799,10 +790,13 @@ namespace Barotrauma } if (character.TeamID == CharacterTeamType.FriendlyNPC) { - // Confiscate stolen goods. + // Confiscate stolen goods and all weapons foreach (var item in Enemy.Inventory.AllItemsMod) { - if (item.StolenDuringRound) + if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound || + item.HasTag("weapon") || + item.GetComponent() != null || + item.GetComponent() != null) { item.Drop(character); character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot); @@ -894,7 +888,8 @@ namespace Barotrauma if (ammunitionIdentifiers != null) { // Try reload ammunition from inventory - ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true); + bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); + ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true); if (ammunition != null) { var container = Weapon.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 705ad946b..b7d66bb08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -7,7 +7,7 @@ namespace Barotrauma { class AIObjectiveContainItem: AIObjective { - public override string DebugTag => "contain item"; + public override string Identifier { get; set; } = "contain item"; public Func GetItemPriority; @@ -61,10 +61,10 @@ namespace Barotrauma this.container = container; } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) + if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character))) { Abandon = true; return false; @@ -87,11 +87,11 @@ namespace Barotrauma } } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(); + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character); protected override void Act(float deltaTime) { - if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) + if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character)) { Abandon = true; return; @@ -146,7 +146,10 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, - abortCondition = obj => !ItemToContain.IsOwnedBy(character), + AbortCondition = obj => + container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + ItemToContain == null || ItemToContain.Removed || + !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder() }, onAbandon: () => Abandon = true, @@ -170,7 +173,8 @@ namespace Barotrauma ignoredItems = containedItems, AllowToFindDivingGear = AllowToFindDivingGear, AllowDangerousPressure = AllowDangerousPressure, - TargetCondition = ConditionLevel + TargetCondition = ConditionLevel, + ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem) }, onAbandon: () => { Abandon = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 63db2719a..559aad746 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveDecontainItem : AIObjective { - public override string DebugTag => "decontain item"; + public override string Identifier { get; set; } = "decontain item"; public Func GetItemPriority; @@ -59,17 +59,20 @@ namespace Barotrauma this.targetContainer = targetContainer; } - protected override bool Check() => IsCompleted; + protected override bool CheckObjectiveSpecific() => IsCompleted; protected override void Act(float deltaTime) { - Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false); + Item itemToDecontain = + targetItem ?? + sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false); + if (itemToDecontain == null) { Abandon = true; return; } - if (itemToDecontain.IgnoreByAI) + if (itemToDecontain.IgnoreByAI(character)) { Abandon = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs new file mode 100644 index 000000000..7c689df98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; + +namespace Barotrauma +{ + class AIObjectiveEscapeHandcuffs : AIObjective + { + // Used for prisoner escorts to allow them to escape their binds + public override string Identifier { get; set; } = "escape handcuffs"; + public override bool AllowAutomaticItemUnequipping => true; + public override bool AllowOutsideSubmarine => true; + public override bool AllowInAnySub => true; + + private int escapeProgress; + private bool isBeingWatched; + + private bool shouldSwitchTeams; + + const string EscapeTeamChangeIdentifier = "escape"; + + public AIObjectiveEscapeHandcuffs(Character character, AIObjectiveManager objectiveManager, bool shouldSwitchTeams = true, bool beginInstantly = false, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + { + this.shouldSwitchTeams = shouldSwitchTeams; + if (beginInstantly) + { + escapeTimer = EscapeIntervalTimer; + } + } + + public override bool CanBeCompleted => true; + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } + protected override bool CheckObjectiveSpecific() => false; + + // escape timer is set to 60 by default to allow players to locate prisoners in time + private float escapeTimer = 60f; + private const float EscapeIntervalTimer = 7.5f; + + private float updateTimer; + private const float UpdateIntervalTimer = 4f; + + protected override float GetPriority() + { + Priority = !isBeingWatched && character.LockHands ? AIObjectiveManager.LowestOrderPriority - 1 : 0; + return Priority; + } + + public override void Update(float deltaTime) + { + updateTimer -= deltaTime; + if (updateTimer <= 0.0f) + { + if (shouldSwitchTeams) + { + if (!character.LockHands) + { + if (!character.HasTeamChange(EscapeTeamChangeIdentifier)) + { + character.TryAddNewTeamChange(EscapeTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful)); + } + } + else + { + character.TryRemoveTeamChange(EscapeTeamChangeIdentifier); + } + } + + isBeingWatched = false; + foreach (Character otherCharacter in Character.CharacterList) + { + if (HumanAIController.IsActive(otherCharacter) && otherCharacter.TeamID == CharacterTeamType.Team1 && HumanAIController.VisibleHulls.Contains(otherCharacter.CurrentHull)) // hasn't been tested yet + { + isBeingWatched = true; // act casual when player characters are around + escapeProgress = 0; + break; + } + } + updateTimer = UpdateIntervalTimer * Rand.Range(0.75f, 1.25f); + } + } + + protected override void Act(float deltaTime) + { + SteeringManager.Reset(); + + escapeTimer -= deltaTime; + if (escapeTimer <= 0.0f) + { + escapeProgress += Rand.Range(2, 5); + if (escapeProgress > 15) + { + Item handcuffs = character.Inventory.FindItemByTag("handlocker"); + if (handcuffs != null) + { + handcuffs.Drop(character); + } + } + escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f); + } + } + public override void Reset() + { + base.Reset(); + escapeProgress = 0; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 25f2082ee..b6163032a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFire : AIObjective { - public override string DebugTag => "extinguish fire"; + public override string Identifier { get; set; } = "extinguish fire"; public override bool ForceRun => true; public override bool ConcurrentObjectives => true; public override bool KeepDivingGearOn => true; @@ -27,7 +27,7 @@ namespace Barotrauma this.targetHull = targetHull; } - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { @@ -68,7 +68,7 @@ namespace Barotrauma return Priority; } - protected override bool Check() => targetHull.FireSources.None(); + protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None(); private float sinTime; protected override void Act(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 2422534e2..62787477d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFires : AIObjectiveLoop { - public override string DebugTag => "extinguish fires"; + public override string Identifier { get; set; } = "extinguish fires"; public override bool ForceRun => true; public override bool AllowInAnySub => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 8187e6092..14c01cb47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFightIntruders : AIObjectiveLoop { - public override string DebugTag => "fight intruders"; + public override string Identifier { get; set; } = "fight intruders"; protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; @@ -21,13 +21,18 @@ namespace Barotrauma protected override float TargetEvaluation() { - // TODO: sorting criteria - return Targets.None() ? 0 : 100; + if (!character.IsOnPlayerTeam) { 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; } + return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; } protected override AIObjective ObjectiveConstructor(Character target) { - var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier); + AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive; + var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index fd29e08db..76c51c0d9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -7,7 +7,8 @@ namespace Barotrauma { class AIObjectiveFindDivingGear : AIObjective { - public override string DebugTag => $"find diving gear ({gearTag})"; + public override string Identifier { get; set; } = "find diving gear"; + public override string DebugTag => $"{Identifier} ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AbandonWhenCannotCompleteSubjectives => false; @@ -23,7 +24,7 @@ namespace Barotrauma public static string LIGHT_DIVING_GEAR = "lightdiving"; public static string OXYGEN_SOURCE = "oxygensource"; - protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem); + protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -38,7 +39,7 @@ namespace Barotrauma return; } targetItem = character.Inventory.FindItemByTag(gearTag, true); - if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) + if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) { TryAddSubObjective(ref getDivingGear, () => { @@ -48,9 +49,11 @@ namespace Barotrauma } return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { - AllowStealing = true, + AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowToFindDivingGear = false, - AllowDangerousPressure = true + AllowDangerousPressure = true, + EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes, + Wear = true }; }, onAbandon: () => Abandon = true, @@ -58,8 +61,6 @@ namespace Barotrauma } else { - HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource")); - HumanAIController.UnequipEmptyItems(targetItem); // Seek oxygen that has at least 10% condition left, if we are inside a friendly sub. // The margin helps us to survive, because we might need some oxygen before we can find more oxygen. // When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks. @@ -119,6 +120,7 @@ namespace Barotrauma int ReportOxygenTankCount() { + if (character.Submarine != Submarine.MainSub) { return 1; } int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1); if (remainingOxygenTanks == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index e8d0f4fb4..942c934d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveFindSafety : AIObjective { - public override string DebugTag => "find safety"; + public override string Identifier { get; set; } = "find safety"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -32,12 +32,12 @@ namespace Barotrauma public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - protected override bool Check() => false; + protected override bool CheckObjectiveSpecific() => false; public override bool CanBeCompleted => true; private bool resetPriority; - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 3b6b38a69..87fab661f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveFixLeak : AIObjective { - public override string DebugTag => "fix leak"; + public override string Identifier { get; set; } = "fix leak"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; @@ -29,19 +29,22 @@ namespace Barotrauma this.isPriority = isPriority; } - protected override bool Check() => Leak.Open <= 0 || Leak.Removed; + protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed; - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { Priority = 0; Abandon = true; } - else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) + else if (HumanAIController.IsTrueForAnyCrewMember( + other => other != HumanAIController && + other.Character.IsBot && + other.ObjectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && + fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak))) { Priority = 0; - Abandon = true; } else { @@ -86,21 +89,22 @@ namespace Barotrauma Abandon = true; return; } - HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel")); - HumanAIController.UnequipEmptyItems(weldingTool); if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), - onAbandon: () => - { - Abandon = true; - ReportWeldingFuelTankCount(); - }, - onCompleted: () => - { - RemoveSubObjective(ref refuelObjective); - ReportWeldingFuelTankCount(); - }); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + { + RemoveExisting = true + }, + onAbandon: () => + { + Abandon = true; + ReportWeldingFuelTankCount(); + }, + onCompleted: () => + { + RemoveSubObjective(ref refuelObjective); + ReportWeldingFuelTankCount(); + }); void ReportWeldingFuelTankCount() { @@ -141,7 +145,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => { - if (Check()) { IsCompleted = true; } + if (CheckObjectiveSpecific()) { IsCompleted = true; } else { // Failed to operate. Probably too far. @@ -160,7 +164,7 @@ namespace Barotrauma }, onAbandon: () => { - if (Check()) { IsCompleted = true; } + if (CheckObjectiveSpecific()) { IsCompleted = true; } else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2)) { // Too far @@ -191,7 +195,7 @@ namespace Barotrauma // This is an approximation, because we don't know the exact reach until the pose is taken. // And even then the actual range depends on the direction we are aiming to. // Found out that without any multiplier the value (209) is often too short. - return repairTool.Range + armLength * 1.2f; + return repairTool.Range + armLength * 1.3f; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 4e3e1f6a1..5f7d91295 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFixLeaks : AIObjectiveLoop { - public override string DebugTag => "fix leaks"; + public override string Identifier { get; set; } = "fix leaks"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; @@ -40,7 +40,7 @@ namespace Barotrauma { int totalLeaks = Targets.Count(); if (totalLeaks == 0) { return 0; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true); bool anyFixers = otherFixers > 0; if (objectiveManager.IsOrder(this)) { @@ -51,7 +51,7 @@ namespace Barotrauma { int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); int leaks = totalLeaks - secondaryLeaks; - float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1; + float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1; if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) { // Enough fixers @@ -74,7 +74,7 @@ namespace Barotrauma // Don't fix a leak on a wall section set to be ignored if (gap.ConnectedWall != null) { - if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; } + if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI(character))) { return false; } if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; } } if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 45b90df47..870b10333 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -8,11 +8,10 @@ namespace Barotrauma { class AIObjectiveGetItem : AIObjective { - public override string DebugTag => "get item"; + public override string Identifier { get; set; } = "get item"; public override bool AbandonWhenCannotCompleteSubjectives => false; - private readonly bool equip; public HashSet ignoredItems = new HashSet(); public Func GetItemPriority; @@ -45,14 +44,17 @@ namespace Barotrauma /// Is the character allowed to take the item from somewhere else than their own sub (e.g. an outpost) /// public bool AllowStealing { get; set; } - public bool TakeWholeStack { get; set; } + public bool Equip { get; set; } + public bool Wear { get; set; } + + public InvSlotType? EquipSlotType { get; set; } public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; - this.equip = equip; + Equip = equip; originalTarget = targetItem; this.targetItem = targetItem; moveToTarget = targetItem?.GetRootInventoryOwner(); @@ -65,7 +67,7 @@ namespace Barotrauma : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; - this.equip = equip; + Equip = equip; this.identifiersOrTags = identifiersOrTags; this.spawnItemIfNotFound = spawnItemIfNotFound; for (int i = 0; i < identifiersOrTags.Length; i++) @@ -197,7 +199,7 @@ namespace Barotrauma Inventory itemInventory = targetItem.ParentInventory; var slots = itemInventory?.FindIndices(targetItem); - if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true)) + if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true)) { if (TakeWholeStack && slots != null) { @@ -227,7 +229,7 @@ namespace Barotrauma return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) - abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, + AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, SpeakIfFails = false }; }, @@ -303,6 +305,7 @@ namespace Barotrauma if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(character)) { continue; } + if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); @@ -365,19 +368,33 @@ namespace Barotrauma } } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } if (targetItem != null) { - return character.HasItem(targetItem, equip); + if (Equip && EquipSlotType.HasValue) + { + return character.HasEquippedItem(targetItem, EquipSlotType.Value); + } + else + { + return character.HasItem(targetItem, Equip); + } } else if (identifiersOrTags != null) { var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true); if (matchingItem != null) { - return !equip || character.HasEquippedItem(matchingItem); + if (Equip && EquipSlotType.HasValue) + { + return character.HasEquippedItem(matchingItem, EquipSlotType.Value); + } + else + { + return !Equip || character.HasEquippedItem(matchingItem); + } } return false; } @@ -387,7 +404,7 @@ namespace Barotrauma private bool CheckItem(Item item) { if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; } + if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 2afb92417..c7ef4fa86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveGoTo : AIObjective { - public override string DebugTag => "go to"; + public override string Identifier { get; set; } = "go to"; private AIObjectiveFindDivingGear findDivingGear; private readonly bool repeat; @@ -20,10 +20,6 @@ namespace Barotrauma /// Doesn't allow the objective to complete if this condition is false /// public Func requiredCondition; - /// - /// Aborts the objective when this condition is true - /// - public Func abortCondition; public Func endNodeFilter; public Func priorityGetter; @@ -38,6 +34,7 @@ namespace Barotrauma private readonly float minDistance = 50; private readonly float seekGapsInterval = 1; private float seekGapsTimer; + private bool cannotFollow; /// /// Display units @@ -81,7 +78,7 @@ namespace Barotrauma public float? OverridePriority = null; - public override float GetPriority() + protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) @@ -177,6 +174,11 @@ namespace Barotrauma character.AIController.SteeringManager.Reset(); return; } + if (cannotFollow) + { + // Wait + character.AIController.SteeringManager.Reset(); + } waitUntilPathUnreachable -= deltaTime; if (!character.IsClimbing) { @@ -263,16 +265,29 @@ namespace Barotrauma if (findDivingGear != null && !findDivingGear.CanBeCompleted) { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref findDivingGear)); + onAbandon: () => Abort(), + onCompleted: () => + { + cannotFollow = false; + RemoveSubObjective(ref findDivingGear); + }); } else { TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onCompleted: () => RemoveSubObjective(ref findDivingGear)); + onAbandon: () => Abort(), + onCompleted: () => + { + cannotFollow = false; + RemoveSubObjective(ref findDivingGear); + }); } return; } + else + { + cannotFollow = false; + } } if (repeat) { @@ -578,22 +593,15 @@ namespace Barotrauma } } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - // First check the distance - // Then the custom condition - // And finally check if can interact (heaviest) + // First check the distance and then if can interact (heaviest) if (Target == null) { Abandon = true; return false; } - if (abortCondition != null && abortCondition(this)) - { - Abandon = true; - return false; - } if (repeat) { return false; @@ -624,6 +632,18 @@ namespace Barotrauma return IsCompleted; } + private void Abort() + { + if (!objectiveManager.IsOrder(this)) + { + Abandon = true; + } + else + { + cannotFollow = true; + } + } + protected override void OnAbandon() { StopMovement(); @@ -657,6 +677,7 @@ namespace Barotrauma findDivingGear = null; seekGapsTimer = 0; TargetGap = null; + cannotFollow = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 9ec668204..1b7b68af7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveIdle : AIObjective { - public override string DebugTag => "idle"; + public override string Identifier { get; set; } = "idle"; public override bool AllowAutomaticItemUnequipping => true; public override bool AllowInAnySub => true; @@ -21,11 +21,6 @@ namespace Barotrauma set { behavior = value; - if (behavior == BehaviorType.StayInHull && TargetHull == null) - { - DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})"); - behavior = BehaviorType.Passive; - } switch (behavior) { case BehaviorType.Passive: @@ -93,7 +88,7 @@ namespace Barotrauma CalculatePriority(); } - protected override bool Check() => false; + protected override bool CheckObjectiveSpecific() => false; public override bool CanBeCompleted => true; public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } @@ -110,21 +105,11 @@ namespace Barotrauma Priority = 1; } - public override float GetPriority() => Priority; + protected override float GetPriority() => Priority; public override void Update(float deltaTime) { - //if (objectiveManager.CurrentObjective == this) - //{ - // if (randomTimer > 0) - // { - // randomTimer -= deltaTime; - // } - // else - // { - // CalculatePriority(); - // } - //} + // Do nothing. Overrides the inherited devotion calculations. } private float timerMargin; @@ -183,6 +168,11 @@ namespace Barotrauma CleanupItems(deltaTime); + if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null) + { + TargetHull = character.CurrentHull; + } + if (behavior == BehaviorType.StayInHull) { currentTarget = TargetHull; @@ -203,7 +193,7 @@ namespace Barotrauma if (currentTarget != null && !currentTargetIsInvalid) { - if (character.TeamID == CharacterTeamType.FriendlyNPC) + if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) { if (currentTarget.Submarine.TeamID != character.TeamID) { @@ -260,9 +250,9 @@ namespace Barotrauma { //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); - bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID; + bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID; bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node => + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } // Check that there is no unsafe or forbidden hulls on the way to the target @@ -419,7 +409,7 @@ namespace Barotrauma if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } if (character.Submarine == null) { break; } - if (character.TeamID == CharacterTeamType.FriendlyNPC) + if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) { if (hull.Submarine.TeamID != character.TeamID) { @@ -519,13 +509,7 @@ namespace Barotrauma } #endregion - public static bool IsForbidden(Hull hull) - { - if (hull == null) { return true; } - string hullName = hull.RoomName; - if (hullName == null) { return false; } - return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase); - } + public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 9635b79d2..338227596 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -40,7 +40,7 @@ namespace Barotrauma : base(character, objectiveManager, priorityModifier, option) { } protected override void Act(float deltaTime) { } - protected override bool Check() => false; + protected override bool CheckObjectiveSpecific() => false; public override bool CanBeCompleted => true; public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowSubObjectiveSorting => true; @@ -106,7 +106,7 @@ namespace Barotrauma UpdateTargets(); } - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index efacf53b2..6c868993d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -132,14 +132,14 @@ namespace Barotrauma } var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) + if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { continue; } } - var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); + var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier); if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); @@ -184,7 +184,8 @@ namespace Barotrauma { var previousObjective = CurrentObjective; var firstObjective = Objectives.FirstOrDefault(); - if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority) + bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority; + if (currentObjectiveIsOrder) { CurrentObjective = CurrentOrder; } @@ -197,6 +198,14 @@ namespace Barotrauma previousObjective?.OnDeselected(); CurrentObjective?.OnSelected(); GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(character, new object[] + { + NetEntityEvent.Type.ObjectiveManagerState, + currentObjectiveIsOrder ? "order" : "objective" + }); + } } return CurrentObjective; } @@ -269,38 +278,29 @@ namespace Barotrauma public void SortObjectives() { - ForcedOrder?.GetPriority(); - + ForcedOrder?.CalculatePriority(); AIObjective orderWithHighestPriority = null; float highestPriority = 0; foreach (var currentOrder in CurrentOrders) { var orderObjective = currentOrder.Objective; if (orderObjective == null) { continue; } - orderObjective.GetPriority(); + orderObjective.CalculatePriority(); if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority) { orderWithHighestPriority = orderObjective; highestPriority = orderObjective.Priority; } } -#if SERVER - if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder) - { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState }); - } -#endif CurrentOrder = orderWithHighestPriority; - for (int i = Objectives.Count - 1; i >= 0; i--) { - Objectives[i].GetPriority(); + Objectives[i].CalculatePriority(); } if (Objectives.Any()) { Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); } - GetCurrentObjective()?.SortSubObjectives(); } @@ -380,7 +380,7 @@ namespace Barotrauma } } - var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); + var newCurrentOrder = CreateObjective(order, option, orderGiver); if (newCurrentOrder != null) { CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); @@ -441,7 +441,7 @@ namespace Barotrauma } } - public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1) + public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1) { if (order == null || order.Identifier == "dismissed") { return null; } AIObjective newObjective; @@ -482,7 +482,6 @@ namespace Barotrauma newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item) { RelevantSkill = order.AppropriateSkill, - RequireAdequateSkills = isAutonomous }; break; case "pumpwater": @@ -492,7 +491,7 @@ namespace Barotrauma newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) { IsLoop = true, - Override = orderGiver != null && orderGiver.IsPlayer + Override = orderGiver != null && orderGiver.IsCommanding }; // ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs, // if we want that the bot just switches the pump on/off and continues doing something else. @@ -519,7 +518,7 @@ namespace Barotrauma { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsPlayer + Override = orderGiver != null && orderGiver.IsCommanding }; break; case "setchargepct": @@ -563,6 +562,9 @@ namespace Barotrauma newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier); } break; + case "escapehandcuffs": + newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); + break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } @@ -571,16 +573,22 @@ namespace Barotrauma { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsPlayer + Override = orderGiver != null && orderGiver.IsCommanding }; if (newObjective.Abandon) { return null; } break; } + if (newObjective != null) + { + newObjective.Identifier = order.Identifier; + } + newObjective.IgnoreAtOutpost = order.IgnoreAtOutpost; return newObjective; } private bool IsAllowedToWait() { + if (!character.IsOnPlayerTeam) { return false; } if (HasOrders()) { return false; } if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; } if (character.AnimController.InWater) { return false; } @@ -606,7 +614,11 @@ namespace Barotrauma /// /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. /// - public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + public IEnumerable GetActiveObjectives() where T : AIObjective + { + if (CurrentObjective == null) { return Enumerable.Empty(); } + return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + } public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); @@ -627,7 +639,10 @@ namespace Barotrauma public float GetOrderPriority(AIObjective objective) { - if (objective == ForcedOrder) { return HighestOrderPriority; } + if (objective == ForcedOrder) + { + return HighestOrderPriority; + } var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective); if (currentOrder.Objective == null) { @@ -635,7 +650,15 @@ namespace Barotrauma } else if (currentOrder.ManualPriority > 0) { - return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority)); + if (objective.ForceHighestPriority) + { + return HighestOrderPriority; + } + if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any()) + { + return HighestOrderPriority; + } + return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority - 1, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority)); } #if DEBUG DebugConsole.AddWarning("Error in order priority: shouldn't return 0!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index de9a89281..5db86b049 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -8,15 +8,18 @@ namespace Barotrauma { class AIObjectiveOperateItem : AIObjective { - public override string DebugTag => $"operate item {component.Name}"; + public override string Identifier { get; set; } = "operate item"; + public override string DebugTag => $"{Identifier} {component.Name}"; + public override bool AllowAutomaticItemUnequipping => true; public override bool AllowMultipleInstances => true; public override bool AllowInAnySub => true; + public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret); - private ItemComponent component, controller; - private Entity operateTarget; - private bool requireEquip; - private bool useController; + private readonly ItemComponent component, controller; + private readonly Entity operateTarget; + private readonly bool requireEquip; + private readonly bool useController; private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; @@ -34,7 +37,7 @@ namespace Barotrauma public Func completionCondition; private bool isDoneOperating; - public override float GetPriority() + protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed || character.LockHands) @@ -43,7 +46,7 @@ namespace Barotrauma Abandon = !isOrder; return Priority; } - if (component.Item.ConditionPercentage <= 0) + if (!isOrder && component.Item.ConditionPercentage <= 0) { Priority = 0; } @@ -100,12 +103,22 @@ namespace Barotrauma break; } } + else if (!isOrder) + { + var steering = component?.Item.GetComponent(); + if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain))) + { + // Ignore if already set to autopilot or if there's a captain onboard + Priority = 0; + return Priority; + } + } if (targetItem.CurrentHull == null || targetItem.Submarine != character.Submarine && !isOrder || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _) || Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)) - || component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI)) + || component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character)) { Priority = 0; } @@ -121,10 +134,10 @@ namespace Barotrauma { float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier); float max = AIObjectiveManager.LowestOrderPriority - 1; - if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup") + if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup") { - // Decrease the priority when targeting a reactor that is already on. - value /= 2; + // Already on, no need to operate. + value = 0; } Priority = MathHelper.Clamp(value, 0, max); } @@ -268,7 +281,7 @@ namespace Barotrauma } } - protected override bool Check() => isDoneOperating && !IsLoop; + protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop; public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 22a08c997..720cbd730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectivePumpWater : AIObjectiveLoop { - public override string DebugTag => "pump water"; + public override string Identifier { get; set; } = "pump water"; public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; @@ -27,7 +27,7 @@ namespace Barotrauma protected override bool Filter(Pump pump) { if (pump == null) { return false; } - if (pump.Item.IgnoreByAI) { return false; } + if (pump.Item.IgnoreByAI(character)) { return false; } if (!pump.Item.IsInteractable(character)) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 6e4046bb5..8e1c022ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -1,6 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; @@ -8,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRepairItem : AIObjective { - public override string DebugTag => "repair item"; + public override string Identifier { get; set; } = "repair item"; public override bool AllowInAnySub => true; @@ -31,9 +32,9 @@ namespace Barotrauma this.isPriority = isPriority; } - public override float GetPriority() + protected override float GetPriority() { - if (!IsAllowed || Item.IgnoreByAI) + if (!IsAllowed || Item.IgnoreByAI(character)) { Priority = 0; Abandon = true; @@ -43,11 +44,10 @@ namespace Barotrauma } return Priority; } - // TODO: priority list? - // Ignore items that are being repaired by someone else. - if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) + if (HumanAIController.IsItemRepairedByAnother(Item, out _)) { Priority = 0; + IsCompleted = true; } else { @@ -66,12 +66,25 @@ namespace Barotrauma float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = isPriority ? 1 : isSelected ? 2 : 3; float max = AIObjectiveManager.LowestOrderPriority - reduction; - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); + float highestWeight = -1; + foreach (string tag in Item.Prefab.Tags) + { + if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight) + { + highestWeight = weight; + } + } + if (highestWeight == -1) + { + // Predefined weight not found. + highestWeight = 1; + } + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * highestWeight * PriorityModifier), 0, 1)); } return Priority; } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) @@ -122,8 +135,6 @@ namespace Barotrauma Abandon = true; return; } - HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel")); - HumanAIController.UnequipEmptyItems(repairTool.Item); RelatedItem item = null; Item fuel = null; foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) @@ -135,9 +146,12 @@ namespace Barotrauma if (fuel == null) { RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), - onCompleted: () => RemoveSubObjective(ref refuelObjective), - onAbandon: () => Abandon = true); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + { + RemoveExisting = true + }, + onCompleted: () => RemoveSubObjective(ref refuelObjective), + onAbandon: () => Abandon = true); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 1c020742f..f549b42c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -9,31 +9,26 @@ namespace Barotrauma { class AIObjectiveRepairItems : AIObjectiveLoop { - public override string DebugTag => "repair items"; - - /// - /// Should the character only attempt to fix items they have the skills to fix, or any damaged item - /// - public bool RequireAdequateSkills; + public override string Identifier { get; set; } = "repair items"; /// /// If set, only fix items where required skill matches this. /// public string RelevantSkill; - private readonly Item prioritizedItem; + public Item PrioritizedItem { get; private set; } public override bool AllowMultipleInstances => true; public override bool AllowInAnySub => true; public readonly static float RequiredSuccessFactor = 0.4f; - public override bool IsDuplicate(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills; + public override bool IsDuplicate(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this); public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) : base(character, objectiveManager, priorityModifier) { - this.prioritizedItem = prioritizedItem; + PrioritizedItem = prioritizedItem; } protected override void CreateObjectives() @@ -69,25 +64,36 @@ namespace Barotrauma protected override bool Filter(Item item) { - if (!IsValidTarget(item, character)) { return false; } - if (item.CurrentHull.FireSources.Count > 0) { return false; } - // Don't repair items in rooms that have enemies inside. - if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + if (!ViableForRepair(item, character, HumanAIController)) { return false; }; if (!Objectives.ContainsKey(item)) { if (item != character.SelectedConstruction) { - float condition = item.ConditionPercentage; - if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; } + if (NearlyFullCondition(item)) { return false; } } } if (!string.IsNullOrWhiteSpace(RelevantSkill)) { if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } } + return !HumanAIController.IsItemRepairedByAnother(item, out _); + } + + public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController) + { + if (!IsValidTarget(item, character)) { return false; } + if (item.CurrentHull.FireSources.Count > 0) { return false; } + // Don't repair items in rooms that have enemies inside. + if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !humanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } return true; } + public static bool NearlyFullCondition(Item item) + { + float condition = item.ConditionPercentage; + return item.Repairables.All(r => condition >= r.RepairThreshold); + } + protected override float TargetEvaluation() { var selectedItem = character.SelectedConstruction; @@ -115,14 +121,7 @@ namespace Barotrauma // Enough fixers return 0; } - if (RequireAdequateSkills) - { - return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio; - } - else - { - return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; - } + return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio; } } @@ -140,7 +139,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) - => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem); + => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem); protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); @@ -148,7 +147,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character) { if (item == null) { return false; } - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3c0fdd972..1c7cdc622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRescue : AIObjective { - public override string DebugTag => "rescue"; + public override string Identifier { get; set; } = "rescue"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; @@ -374,7 +374,7 @@ namespace Barotrauma } } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { @@ -390,6 +390,7 @@ namespace Barotrauma bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) || targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold); + if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), @@ -398,7 +399,7 @@ namespace Barotrauma return isCompleted; } - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 70147a07a..73115b133 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -7,7 +7,7 @@ namespace Barotrauma { class AIObjectiveRescueAll : AIObjectiveLoop { - public override string DebugTag => "rescue all"; + public override string Identifier { get; set; } = "rescue all"; public override bool ForceRun => true; public override bool InverseTargetEvaluation => true; public override bool AllowOutsideSubmarine => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index fd318964e..09a73221f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -150,16 +150,21 @@ namespace Barotrauma //legacy support public readonly string[] AppropriateJobs; public readonly string[] Options; + public readonly string[] HiddenOptions; + public readonly string[] AllOptions; private readonly Dictionary OptionNames; public readonly Dictionary OptionSprites; - private readonly Dictionary minimapIcons; - public Dictionary MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons; - public readonly bool MustSetTarget; + /// + /// Can the order be turned into a non-entity-targeting one if it was originally created with a target entity. + /// Note: if MustSetTarget is true, CanBeGeneralized will always be false. + /// + public readonly bool CanBeGeneralized; public readonly string AppropriateSkill; public readonly bool Hidden; + public readonly bool IgnoreAtOutpost; public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; public bool IsPrefab { get; private set; } @@ -307,11 +312,15 @@ namespace Barotrauma TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); + HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); + AllOptions = Options.Concat(HiddenOptions).ToArray(); var category = orderElement.GetAttributeString("category", null); if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); + CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true); AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); Hidden = orderElement.GetAttributeBool("hidden", false); + IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false); var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? orderElement.GetAttributeStringArray("optionnames", new string[0]); @@ -348,15 +357,6 @@ namespace Barotrauma } } - minimapIcons = new Dictionary(); - var minimapIconElements = orderElement.GetChildElements("minimapicon"); - foreach (XElement minimapIconElement in minimapIconElements) - { - var id = minimapIconElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true)); - } - IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; @@ -366,7 +366,7 @@ namespace Barotrauma /// /// Constructor for order instances /// - public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null) { Prefab = prefab.Prefab ?? prefab; @@ -384,12 +384,14 @@ namespace Barotrauma AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; MustSetTarget = prefab.MustSetTarget; + CanBeGeneralized = prefab.CanBeGeneralized; AppropriateSkill = prefab.AppropriateSkill; Category = prefab.Category; MustManuallyAssign = prefab.MustManuallyAssign; IsIgnoreOrder = prefab.IsIgnoreOrder; DrawIconWhenContained = prefab.DrawIconWhenContained; Hidden = prefab.Hidden; + IgnoreAtOutpost = prefab.IgnoreAtOutpost; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -413,12 +415,18 @@ namespace Barotrauma IsPrefab = false; } + /// + /// Constructor for order instances + /// public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver) { TargetPosition = target; TargetType = OrderTargetType.Position; } + /// + /// Constructor for order instances + /// public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver) { WallSectionIndex = sectionIndex; @@ -487,27 +495,19 @@ namespace Barotrauma if (submarine == null) { return matchingItems; } if (ItemComponentType != null || TargetItems.Length > 0) { - matchingItems = TargetItems.Length > 0 ? - Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) : - Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _)); - if (mustBelongToPlayerSub) + foreach (var item in Item.ItemList) { - matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player); - } - matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); - if (requiredTeam.HasValue) - { - matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value); - } - matchingItems.RemoveAll(it => it.NonInteractable); - if (UseController) - { - matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _)); - } - if (interactableFor != null) - { - matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) || - (UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor))); + if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; } + if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; } + if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; } + if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; } + if (item.NonInteractable) { continue; } + if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; } + Controller controller = null; + if (UseController && !item.TryFindController(out controller)) { continue; } + if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; } + matchingItems.Add(item); } } return matchingItems; @@ -525,7 +525,15 @@ namespace Barotrauma public string GetOptionName(string id) { - return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id]; + if (Prefab == null) + { + if (OptionNames.ContainsKey(id)) { return OptionNames[id]; } + } + else + { + if (Prefab.OptionNames.ContainsKey(id)) { return Prefab.OptionNames[id]; } + } + return string.Empty; } public string GetOptionName(int index) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 08ed530f3..82b4ed49b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -410,7 +410,10 @@ namespace Barotrauma if (end.state == 0 || end.Parent == null) { #if DEBUG - DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow); + if (errorMsgStr != null) + { + DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow); + } #endif return new SteeringPath(true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 5c653e32d..07104f091 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -370,7 +370,7 @@ namespace Barotrauma if (c.Inventory != null) { var inventoryElement = new XElement("inventory"); - c.SaveInventory(c.Inventory, inventoryElement); + Character.SaveInventory(c.Inventory, inventoryElement); petElement.Add(inventoryElement); } @@ -400,7 +400,7 @@ namespace Barotrauma spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } var pet = Character.Create(speciesName, spawnPos, seed); - var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; + var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior; if (petBehavior != null) { petBehavior.Owner = owner; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs new file mode 100644 index 000000000..8e4a6d4e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -0,0 +1,132 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + abstract class ShipIssueWorker + { + public const float MaxImportance = 100f; + public const float MinImportance = 0f; + public Order SuggestedOrderPrefab { get; } + + private float importance; + public float Importance + { + get + { + return importance; + } + set + { + importance = MathHelper.Clamp(value, MinImportance, MaxImportance); + } + } + public float CurrentRedundancy { get; set; } + + public readonly ShipCommandManager shipCommandManager; + public string Option { get; set; } + public Character OrderedCharacter { get; set; } + public Order CurrentOrder { get; private set; } + public ItemComponent TargetItemComponent { get; protected set; } + public Item TargetItem { get; protected set; } + public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected + + protected virtual Character CommandingCharacter => shipCommandManager.character; + public virtual float TimeSinceLastAttempt { get; set; } + public virtual float RedundantIssueModifier => 0.5f; + public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies + public virtual bool AllowEasySwitching => false; + + public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null) + { + this.shipCommandManager = shipCommandManager; + SuggestedOrderPrefab = suggestedOrderPrefab; + Option = option; + } + + public void SetOrder(Character orderedCharacter) + { + OrderedCharacter = orderedCharacter; + if (orderedCharacter != CommandingCharacter) + { + CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false)); + } + + // not sure if new orders are supposed to be created each time. TODO m61: check later + CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); + OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter); + TimeSinceLastAttempt = 0f; + } + + public void RemoveOrder() + { + OrderedCharacter = null; + CurrentOrder = null; + } + + protected virtual bool IsIssueViable() + { + return true; + } + + public float CalculateImportance(bool isEmergency) + { + Importance = 0f; // reset anything that needs resetting + + if (!Active) + { + return Importance; + } + + Active = IsIssueViable(); + + if (isEmergency && StopDuringEmergency) + { + return Importance; + } + + CalculateImportanceSpecific(); + + // if there are other orders of the same type already being attended to, such as fixing leaks + // reduce the relative importance of this issue + CurrentRedundancy = 1f; + foreach (ShipIssueWorker shipIssueWorker in shipCommandManager.ShipIssueWorkers) + { + if (shipIssueWorker.GetType() == GetType() && shipIssueWorker != this && shipIssueWorker.OrderAttendedTo()) + { + CurrentRedundancy *= RedundantIssueModifier; + } + } + Importance *= CurrentRedundancy; + + return Importance; + } + + public bool OrderAttendedTo(float timeSinceLastCheck = 0f) + { + if (!HumanAIController.IsActive(OrderedCharacter)) + { + return false; + } + + // accept only the highest priority order + if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder) + { +#if DEBUG + ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); +#endif + return false; + } + + if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter)) + { +#if DEBUG + ShipCommandManager.ShipCommandLog(OrderedCharacter + " was unable to perform assigned order in " + this); +#endif + return false; + } + return true; + } + public abstract void CalculateImportanceSpecific(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerFixLeaks.cs new file mode 100644 index 000000000..2308b90ab --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerFixLeaks.cs @@ -0,0 +1,38 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class ShipGlobalIssueFixLeaks : ShipGlobalIssue + { + readonly List hullSeverities = new List(); + public ShipGlobalIssueFixLeaks(ShipCommandManager shipCommandManager) : base(shipCommandManager) { } + public override void CalculateGlobalIssue() + { + hullSeverities.Clear(); + + foreach (Gap gap in Gap.GapList) + { + if (AIObjectiveFixLeaks.IsValidTarget(gap, shipCommandManager.character)) + { + hullSeverities.Add(AIObjectiveFixLeaks.GetLeakSeverity(gap)); + } + } + + float averagePercentage = 0f; + if (hullSeverities.Any()) + { + hullSeverities.Sort(); + averagePercentage = hullSeverities.TakeLast(3).Average(); // get the 3 most damaged items on the ship and get their average + } + GlobalImportance = averagePercentage; + } + } + + class ShipIssueWorkerFixLeaks : ShipIssueWorkerGlobal + { + public override bool StopDuringEmergency => false; + public ShipIssueWorkerFixLeaks(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks) : base(shipCommandManager, order, shipGlobalIssueFixLeaks) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerGlobal.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerGlobal.cs new file mode 100644 index 000000000..d129e0f43 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerGlobal.cs @@ -0,0 +1,29 @@ +namespace Barotrauma +{ + abstract class ShipGlobalIssue + { + public float GlobalImportance { get; set; } + + protected ShipCommandManager shipCommandManager; + public ShipGlobalIssue(ShipCommandManager shipCommandManager) + { + this.shipCommandManager = shipCommandManager; + } + public abstract void CalculateGlobalIssue(); + } + + abstract class ShipIssueWorkerGlobal : ShipIssueWorker + { + private readonly ShipGlobalIssue shipGlobalIssue; + + public ShipIssueWorkerGlobal(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, ShipGlobalIssue shipGlobalIssue) : base (shipCommandManager, suggestedOrderPrefab) + { + this.shipGlobalIssue = shipGlobalIssue; + } + + public override void CalculateImportanceSpecific() // importances for global issues are precalculated, so that they don't need to be calculated per each attending character + { + Importance = shipGlobalIssue.GlobalImportance; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs new file mode 100644 index 000000000..f588de938 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs @@ -0,0 +1,30 @@ +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + abstract class ShipIssueWorkerItem : ShipIssueWorker + { + public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option) + { + TargetItemComponent = targetItemComponent; + TargetItem = targetItem; + } + + protected override bool IsIssueViable() + { + if (TargetItemComponent == null) + { + DebugConsole.ThrowError("TargetItemComponent was null in " + this); + return false; + } + + if (TargetItem == null) + { + DebugConsole.ThrowError("TargetItem was null in " + this); + return false; + } + + return true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs new file mode 100644 index 000000000..1d4e55f2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -0,0 +1,41 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem + { + public override float RedundantIssueModifier => 0.65f; + private readonly List targetingImportances = new List(); + + public override bool AllowEasySwitching => true; + + public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { } + + float GetTargetingImportance(Entity entity) + { + float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition); + return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance); + } + + public override void CalculateImportanceSpecific() + { + if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; } + + targetingImportances.Clear(); + foreach (Character character in shipCommandManager.EnemyCharacters) + { + targetingImportances.Add(GetTargetingImportance(character)); + } + // there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing + + if (targetingImportances.Any()) + { + targetingImportances.Sort(); + Importance = targetingImportances.TakeLast(3).Average(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs new file mode 100644 index 000000000..9f6cbe6ec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs @@ -0,0 +1,21 @@ +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem + { + public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) + { + } + + public override void CalculateImportanceSpecific() + { + if (TargetItem.Condition <= 0f) { return; } + + if (TargetItemComponent is Reactor reactor && -reactor.CurrPowerConsumption < float.Epsilon) + { + Importance = 40f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerRepairSystems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerRepairSystems.cs new file mode 100644 index 000000000..72f82be2a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerRepairSystems.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class ShipGlobalIssueRepairSystems : ShipGlobalIssue + { + readonly List itemsNeedingRepair = new List(); + + public ShipGlobalIssueRepairSystems(ShipCommandManager shipCommandManager) : base(shipCommandManager) { } + + public override void CalculateGlobalIssue() + { + itemsNeedingRepair.Clear(); + + foreach (Item item in shipCommandManager.CommandedSubmarine.GetItems(true)) + { + if (!AIObjectiveRepairItems.ViableForRepair(item, shipCommandManager.character, shipCommandManager.character.AIController as HumanAIController)) { continue; } + if (AIObjectiveRepairItems.NearlyFullCondition(item)) { continue; } + itemsNeedingRepair.Add(item); + // merged this logic with AIObjectiveRepairItems + } + + if (itemsNeedingRepair.Any()) + { + itemsNeedingRepair.Sort((x, y) => y.ConditionPercentage.CompareTo(x.ConditionPercentage)); + float modifiedPercentage = itemsNeedingRepair.TakeLast(3).Average(x => x.ConditionPercentage) * 0.6f + itemsNeedingRepair.TakeLast(10).Average(x => x.ConditionPercentage) * 0.4f; + // calculate a modified percentage with the most damaged items, with 60% the weight given to the top 3 damaged and the remaining given to top 10 + GlobalImportance = 100 - modifiedPercentage; + } + // this system works reasonably well, though it could give extra importance to repairing critical items like reactors and junction boxes + } + } + + class ShipIssueWorkerRepairSystems : ShipIssueWorkerGlobal // this class could be removed, but it might need special behavior later + { + public ShipIssueWorkerRepairSystems(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems) : base(shipCommandManager, order, shipGlobalIssueRepairSystems) + { + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs new file mode 100644 index 000000000..e69eba53f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs @@ -0,0 +1,20 @@ +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + class ShipIssueWorkerSteer : ShipIssueWorkerItem + { + // The AI could be set to steer automatically through a specialized job or autonomous objectives + // but the logic involved doesn't really allow that without some annoyingly specific changes + // hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong + public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { } + public override void CalculateImportanceSpecific() + { + if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; } + if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; } + if (TargetItem.Condition <= 0f) { return; } + + Importance = 70f; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs new file mode 100644 index 000000000..23856db00 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -0,0 +1,383 @@ +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + class ShipCommandManager + { + public readonly Character character; + public readonly HumanAIController humanAIController; + + private bool active; + public bool Active + { + get { return active; } + set + { + active = value ? TryInitializeShipCommandManager() : value; + } + } + + public Submarine EnemySubmarine + { + get; + private set; + } + + public Submarine CommandedSubmarine + { + get; + private set; + } + + private Steering steering; + public readonly List patrolPositions = new List(); + public enum NavigationStates + { + Inactive, + Patrol, + Aggressive + } + + public NavigationStates NavigationState { get; private set; } = NavigationStates.Inactive; + + float navigationTimer = 0f; + private readonly float navigationInterval = 4f; + + float timeUntilRam; + private const float RamTimerMax = 17.5f; + + public readonly List ShipIssueWorkers = new List(); + private const float MinimumIssueThreshold = 10f; + private const float IssueDevotionBuffer = 5f; + + private float decisionTimer = 6f; + private readonly float decisionInterval = 6f; + + private float timeSinceLastCommandDecision; + private float timeSinceLastNavigation; + + public readonly List AlliedCharacters = new List(); + public readonly List EnemyCharacters = new List(); + + private readonly List attendedIssues = new List(); + private readonly List availableIssues = new List(); + private readonly List shipGlobalIssues = new List(); + + public ShipCommandManager(Character character) + { + this.character = character; + humanAIController = character.AIController as HumanAIController; + } + + public void Update(float deltaTime) + { + if (!Active) { return; } + decisionTimer -= deltaTime; + if (decisionTimer <= 0.0f) + { + UpdateCommandDecision(timeSinceLastCommandDecision); + decisionTimer = decisionInterval * Rand.Range(0.8f, 1.2f); + timeSinceLastCommandDecision = decisionTimer; + } + + navigationTimer -= deltaTime; + if (navigationTimer <= 0.0f) + { + UpdateNavigation(timeSinceLastNavigation); + navigationTimer = navigationInterval * Rand.Range(0.8f, 1.2f); + timeSinceLastNavigation = navigationTimer; + } + } + + public static void ShipCommandLog(string text) + { + if (GameSettings.VerboseLogging) + { + DebugConsole.NewMessage(text); + } + } + + static bool WithinRange(float range, float distanceSquared) + { + return range * range > distanceSquared; + } + + void UpdateNavigation(float timeSinceLastUpdate) + { + if (steering == null || EnemySubmarine == null) + { + return; + } + + float distanceSquaredEnemy = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, EnemySubmarine.WorldPosition); + + if (NavigationState != NavigationStates.Aggressive) + { + if (WithinRange(7000f, distanceSquaredEnemy)) + { +#if DEBUG + ShipCommandLog("Ship " + CommandedSubmarine + " was within the aggro range of " + EnemySubmarine); +#endif + NavigationState = NavigationStates.Aggressive; + } + else if (WithinRange(40000f, distanceSquaredEnemy)) + { + NavigationState = NavigationStates.Patrol; + } + } + + if (NavigationState == NavigationStates.Aggressive) + { + steering.AITacticalTarget = EnemySubmarine.WorldPosition; + if (WithinRange(8500f, distanceSquaredEnemy) && !WithinRange(1500f, distanceSquaredEnemy)) // if we are within enemy ship's range for ramTimerMax, try to ram them instead (if we're not already very close) + { + if (steering.AIRamTimer > 0f) + { +#if DEBUG + ShipCommandLog("Ship " + CommandedSubmarine + " was still ramming, " + steering.AIRamTimer + " left"); +#endif + } + else + { + timeUntilRam -= timeSinceLastUpdate; +#if DEBUG + ShipCommandLog("Ship " + CommandedSubmarine + " was close enough to ram, " + timeUntilRam + " left until ramming"); +#endif + + if (timeUntilRam <= 0f) + { +#if DEBUG + ShipCommandLog("Ship " + CommandedSubmarine + " is attempting to ram!"); +#endif + steering.AIRamTimer = 50f; + timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f); + } + } + } + else + { + steering.AIRamTimer = 0f; + timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f); + } + } + else if (patrolPositions.Any()) + { + float distanceSquaredPatrol = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, patrolPositions.First()); + + if (WithinRange(7000f, distanceSquaredPatrol)) + { + Vector2 lastPosition = patrolPositions.First(); + patrolPositions.RemoveAt(0); + patrolPositions.Add(lastPosition); + } + steering.AITacticalTarget = patrolPositions.First(); + } + } + + public bool AbleToTakeOrder(Character character) + { + return !character.IsIncapacitated && !character.LockHands && character.Submarine == CommandedSubmarine; + } + + void UpdateCommandDecision(float timeSinceLastUpdate) + { + +#if DEBUG + ShipCommandLog("Updating command for character " + character); +#endif + + shipGlobalIssues.ForEach(c => c.CalculateGlobalIssue()); + + AlliedCharacters.Clear(); + EnemyCharacters.Clear(); + + bool isEmergency = false; + + foreach (Character potentialCharacter in Character.CharacterList) + { + if (!HumanAIController.IsActive(character)) { continue; } + + if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController) + { + if (AbleToTakeOrder(potentialCharacter)) + { + AlliedCharacters.Add(potentialCharacter); + } + } + else + { + EnemyCharacters.Add(potentialCharacter); + if (potentialCharacter.Submarine == CommandedSubmarine) // if enemies are on board, don't issue normal orders anymore + { + isEmergency = true; + } + } + } + + attendedIssues.Clear(); + availableIssues.Clear(); + + foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers) + { + float importance = shipIssueWorker.CalculateImportance(isEmergency); + if (shipIssueWorker.OrderAttendedTo(timeSinceLastUpdate)) + { +#if DEBUG + ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter); +#endif + attendedIssues.Add(shipIssueWorker); + } + else + { +#if DEBUG + ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to"); +#endif + shipIssueWorker.RemoveOrder(); + availableIssues.Add(shipIssueWorker); + } + } + + availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance)); + attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance)); + + ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault(); + + float bestValue = 0f; + Character bestCharacter = null; + + if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold) + { + IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true); + + foreach (Character orderedCharacter in bestCharacters) + { + float issueApplicability = mostImportantIssue.Importance; + + // prefer not to switch if not qualified + issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f; + + ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter); + + if (occupiedIssue != null) + { + if (occupiedIssue.GetType() == mostImportantIssue.GetType() && mostImportantIssue is ShipIssueWorkerGlobal && occupiedIssue is ShipIssueWorkerGlobal) + { + continue; + } + + // reverse redundancy to ensure certain issues can be switched over easily (operating weapons) + if (mostImportantIssue.AllowEasySwitching && occupiedIssue.AllowEasySwitching) + { + issueApplicability /= mostImportantIssue.CurrentRedundancy; + } + + // give slight preference if not qualified for current job + issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f; + + // prefer not to switch orders unless considerably more important + issueApplicability -= IssueDevotionBuffer; + + if (issueApplicability + IssueDevotionBuffer < occupiedIssue.Importance) + { + continue; + } + } + + // prefer first one in bestCharacters in tiebreakers + if (issueApplicability > bestValue) + { + bestValue = issueApplicability; + bestCharacter = orderedCharacter; + } + } + } + + if (bestCharacter != null && mostImportantIssue != null) + { +#if DEBUG + ShipCommandLog("Setting " + mostImportantIssue + " for character " + bestCharacter); +#endif + mostImportantIssue.SetOrder(bestCharacter); + } + else // if we didn't give an order, let's try to dismiss someone instead + { + foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers) + { + if (shipIssueWorker.Importance <= 0f && shipIssueWorker.OrderAttendedTo()) + { +#if DEBUG + ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); +#endif + Order orderPrefab = Order.GetPrefab("dismissed"); + character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); + shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character); + shipIssueWorker.RemoveOrder(); + break; + } + } + } + } + + bool TryInitializeShipCommandManager() + { + CommandedSubmarine = character.Submarine; + + if (CommandedSubmarine == null) + { + DebugConsole.ThrowError("TryInitializeShipCommandManager failed: CommandedSubmarine was null for character " + character); + return false; + } + + EnemySubmarine = Submarine.MainSubs[0] == CommandedSubmarine ? Submarine.MainSubs[1] : Submarine.MainSubs[0]; + + if (EnemySubmarine == null) + { + DebugConsole.ThrowError("TryInitializeShipCommandManager failed: EnemySubmarine was null for character " + character); + return false; + } + + timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f); + + ShipIssueWorkers.Clear(); + + // could have support for multiple reactors, todo m61 + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + { + ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup")); + } + + if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) + { + steering = steeringComponent; + ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical")); + } + + foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) + { + ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent())); + } + + int crewSizeModifier = 2; + // these issueworkers revolve around a singular, shared issue, which is injected into them to prevent redundant calculations + ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this); + for (int i = 0; i < crewSizeModifier; i++) + { + ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks)); + } + shipGlobalIssues.Add(shipGlobalIssueFixLeaks); + + ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this); + for (int i = 0; i < crewSizeModifier; i++) + { + ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems)); + } + shipGlobalIssues.Add(shipGlobalIssueRepairSystems); + + return true; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 09a0fd3f4..e7df09aa5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -37,7 +37,14 @@ namespace Barotrauma private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); - public WreckAI(Submarine wreck) + public static WreckAI Create(Submarine wreck) + { + var wreckAI = new WreckAI(wreck); + if (wreckAI.Config == null) { return null; } + return wreckAI; + } + + private WreckAI(Submarine wreck) { Wreck = wreck; Config = WreckAIConfig.GetRandom(); @@ -55,37 +62,59 @@ namespace Barotrauma } allItems = Wreck.GetItems(false); thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab)); - var hulls = Wreck.GetHulls(false); + hulls.AddRange(Wreck.GetHulls(false)); + var potentialBrainHulls = new Dictionary(); brain = new Item(brainPrefab, Vector2.Zero, Wreck); thalamusItems.Add(brain); - Vector2 negativeMargin = new Vector2(40, 20); - Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin; - Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3); - // First try to get a room that is not too big and not in the edges of the sub. - // Also try not to create the brain in a room that already have carrier items inside. - // Ignore hulls that have any linked hulls to keep the calculations simple. + 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. - // Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room. Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); - bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities().None() && h.ConnectedGaps.None(g => g.Open > 0); - bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y; - bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect); - bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h); - Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); - if (brainHull == null) + foreach (Hull hull in hulls) { - brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); - } - if (brainHull == null) - { - brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server); - } - if (brainHull == null) - { - brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server); + 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)); + float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor; + if (hull.GetLinkedEntities().Any()) + { + // Ignore hulls that have any linked hulls to keep the calculations simple. + continue; + } + else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y))) + { + // Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall. + continue; + } + else if (thalamusItems.Any(i => i.CurrentHull == hull)) + { + // Don't create the brain in a room that already has thalamus items inside it. + continue; + } + else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y) + { + // Don't select too small rooms. + continue; + } + if (weight > 0) + { + potentialBrainHulls.TryAdd(hull, weight); + } } + Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Keys.ToList(), potentialBrainHulls.Values.ToList(), Rand.RandSync.Server); var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p)); - if (brainHull == null) { return; } + if (brainHull == null) + { + DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room."); + brainHull = hulls.GetRandom(Rand.RandSync.Server); + } + if (brainHull == null) + { + DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus."); + return; + } brainHull.WaterVolume = brainHull.Volume; brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); brain.CurrentHull = brainHull; @@ -158,11 +187,12 @@ namespace Barotrauma if (!spawnOrgans.Contains(item)) { spawnOrgans.Add(item); + // Try to flood the hull so that the spawner won't die. + item.CurrentHull.WaterVolume = item.CurrentHull.Volume; } } } wayPoints.AddRange(Wreck.GetWaypoints(false)); - hulls.AddRange(Wreck.GetHulls(false)); IsAlive = true; thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); } @@ -307,9 +337,16 @@ namespace Barotrauma public static void RemoveThalamusItems(Submarine wreck) { + List thalamusItems = new List(); foreach (var wreckAiConfig in WreckAIConfig.List) { - GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove()); + thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity)); + } + thalamusItems = thalamusItems.Distinct().ToList(); + foreach (MapEntity thalamusItem in thalamusItems) + { + thalamusItem.Remove(); + wreck.PhysicsBody.FarseerBody.FixtureList.Where(f => f.UserData == thalamusItem).ForEachMod(f => wreck.PhysicsBody.FarseerBody.Remove(f)); } } @@ -323,15 +360,16 @@ namespace Barotrauma private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom); private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside); private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside); - private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside); - private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside); + private int MinCellsInside => CalculateCellCount(3, Config.MinAgentsInside); + private int MaxCellsInside => CalculateCellCount(5, Config.MaxAgentsInside); private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount); private float MinWaterLevel => Config.MinWaterLevel; private int CalculateCellCount(int minValue, int maxValue) { if (maxValue == 0) { return 0; } - return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier)); + float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier); + return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t)); } private float GetSpawnTime() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 747e56813..8854ba3d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -270,14 +270,16 @@ namespace Barotrauma else { LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); + if (rightWrist != null) + { + forearmLength = Vector2.Distance( + rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, + rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - forearmLength = Vector2.Distance( - rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, - rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - - forearmLength += Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + forearmLength += Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index eae42212a..43d9dc327 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -74,7 +74,7 @@ namespace Barotrauma } } - public bool HasMultipleLimbsOfSameType => Limbs.Length > limbDictionary.Count; + public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count; private bool frozen; public bool Frozen @@ -416,10 +416,7 @@ namespace Barotrauma protected void CreateColliders() { - if (collider != null) - { - collider.ForEach(c => c.Remove()); - } + collider?.ForEach(c => c.Remove()); DebugConsole.Log($"Creating colliders from {RagdollParams.Name}."); collider = new List(); foreach (var cParams in RagdollParams.Colliders) @@ -479,10 +476,7 @@ namespace Barotrauma protected void CreateLimbs() { - if (limbs != null) - { - limbs.ForEach(l => l.Remove()); - } + limbs?.ForEach(l => l.Remove()); DebugConsole.Log($"Creating limbs from {RagdollParams.Name}."); limbDictionary = new Dictionary(); limbs = new Limb[RagdollParams.Limbs.Count]; @@ -1547,6 +1541,7 @@ namespace Barotrauma case Physics.CollisionLevel: if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; } if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; } + if (fixture.IsSensor) { return -1; } if (fraction < standOnFloorFraction) { standOnFloorFraction = fraction; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 247b898b4..c89b02b76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -51,7 +51,7 @@ namespace Barotrauma public readonly Limb HitLimb; public readonly List AppliedDamageModifiers; - + public AttackResult(List afflictions, Limb hitLimb, List appliedDamageModifiers = null) { HitLimb = hitLimb; @@ -137,6 +137,9 @@ namespace Barotrauma set => _itemDamage = value; } + [Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + public float Penetration { get; private set; } + /// /// Currently only used with variants. Used for multiplying all the damage. /// @@ -304,7 +307,7 @@ namespace Barotrauma return totalDamage * DamageMultiplier; } - public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) + public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f) { if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); @@ -314,6 +317,7 @@ namespace Barotrauma DamageRange = range; StructureDamage = LevelWallDamage = structureDamage; ItemDamage = itemDamage; + Penetration = Penetration; } public Attack(XElement element, string parentDebugName) @@ -391,14 +395,12 @@ namespace Barotrauma Affliction affliction; string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); - if (afflictionPrefab != null) + if (afflictionPrefab == null) { - affliction = afflictionPrefab.Instantiate(0.0f); - } - else - { - affliction = new Affliction(null, 0); + DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}"); + continue; } + affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null) @@ -478,8 +480,8 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - effect.GetNearbyTargets(worldPosition, targets); + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); effect.Apply(effectType, deltaTime, targetEntity, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) @@ -492,6 +494,7 @@ namespace Barotrauma return attackResult; } + readonly List targets = new List(); public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null) { if (targetLimb == null) @@ -511,7 +514,7 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb); + var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; foreach (StatusEffect effect in statusEffects) @@ -536,8 +539,8 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - effect.GetNearbyTargets(worldPosition, targets); + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); effect.Apply(effectType, deltaTime, targetLimb.character, targets); } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index a9b8a40e4..1e7a4d476 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -94,7 +94,13 @@ namespace Barotrauma public bool IsLocalPlayer => Controlled == this; public bool IsPlayer => Controlled == this || IsRemotePlayer; + + /// + /// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders. + /// + public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active); public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; + public bool IsEscorted { get; set; } public readonly Dictionary Properties; public Dictionary SerializableProperties @@ -109,6 +115,8 @@ namespace Barotrauma protected Key[] keys; + public HumanPrefab Prefab; + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -120,6 +128,101 @@ namespace Barotrauma } } + protected readonly Dictionary activeTeamChanges = new Dictionary(); + protected ActiveTeamChange currentTeamChange; + const string OriginalTeamIdentifier = "original"; + + public void SetOriginalTeam(CharacterTeamType newTeam) + { + TryRemoveTeamChange(OriginalTeamIdentifier); + currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base); + TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange); + } + + protected void ChangeTeam(CharacterTeamType newTeam) + { + if (newTeam == teamID) + { + return; + } + teamID = newTeam; + if (info != null) { info.TeamID = newTeam; } + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return; + } + // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) + SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, orderGiver: this, speak: false); + +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange }); +#endif + } + + public bool HasTeamChange(string identifier) + { + return activeTeamChanges.ContainsKey(identifier); + } + + public bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange) + { + bool success = activeTeamChanges.TryAdd(identifier, newTeamChange); + if (success) + { + if (currentTeamChange == null) + { + // set team logic to use active team changes as soon as the first team change is added + SetOriginalTeam(TeamID); + } + } + else + { +#if DEBUG + DebugConsole.ThrowError("Tried to add an existing team change! Make sure to check if the team change exists first."); +#endif + } + return success; + } + public bool TryRemoveTeamChange(string identifier) + { + if (activeTeamChanges.TryGetValue(identifier, out ActiveTeamChange removedTeamChange)) + { + if (currentTeamChange == removedTeamChange) + { + currentTeamChange = activeTeamChanges[OriginalTeamIdentifier]; + } + } + return activeTeamChanges.Remove(identifier); + } + + public void UpdateTeam() + { + if (currentTeamChange == null) + { + return; + } + + ActiveTeamChange bestTeamChange = currentTeamChange; + foreach (var desiredTeamChange in activeTeamChanges) // order of iteration matters because newest is preferred when multiple same-priority team changes exist + { + if (bestTeamChange.TeamChangePriority < desiredTeamChange.Value.TeamChangePriority) + { + bestTeamChange = desiredTeamChange.Value; + } + } + if (TeamID != bestTeamChange.DesiredTeamId) + { + ChangeTeam(bestTeamChange.DesiredTeamId); + currentTeamChange = bestTeamChange; + + if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior + { + SetOrder(Order.GetPrefab("fightintruders"), orderOption: null, priority: 3, orderGiver: this, speak: false); + } + } + } + public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; @@ -336,9 +439,10 @@ namespace Barotrauma private Action onCustomInteract; public ConversationAction ActiveConversation; + public bool RequireConsciousnessForCustomInteract = true; public bool AllowCustomInteract { - get { return !IsIncapacitated && Stun <= 0.0f && !Removed; } + get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; } } private float lockHandsTimer; @@ -919,7 +1023,6 @@ namespace Barotrauma { teamID = Info.TeamID; } - keys = new Key[Enum.GetNames(typeof(InputType)).Length]; for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++) { @@ -1119,17 +1222,17 @@ namespace Barotrauma switch (inputType) { case InputType.Left: - return !(dequeuedInput.HasFlag(InputNetFlags.Left)) && (prevDequeuedInput.HasFlag(InputNetFlags.Left)); + return dequeuedInput.HasFlag(InputNetFlags.Left) && !prevDequeuedInput.HasFlag(InputNetFlags.Left); case InputType.Right: - return !(dequeuedInput.HasFlag(InputNetFlags.Right)) && (prevDequeuedInput.HasFlag(InputNetFlags.Right)); + return dequeuedInput.HasFlag(InputNetFlags.Right) && !prevDequeuedInput.HasFlag(InputNetFlags.Right); case InputType.Up: - return !(dequeuedInput.HasFlag(InputNetFlags.Up)) && (prevDequeuedInput.HasFlag(InputNetFlags.Up)); + return dequeuedInput.HasFlag(InputNetFlags.Up) && !prevDequeuedInput.HasFlag(InputNetFlags.Up); case InputType.Down: - return !(dequeuedInput.HasFlag(InputNetFlags.Down)) && (prevDequeuedInput.HasFlag(InputNetFlags.Down)); + return dequeuedInput.HasFlag(InputNetFlags.Down) && !prevDequeuedInput.HasFlag(InputNetFlags.Down); case InputType.Run: - return !(dequeuedInput.HasFlag(InputNetFlags.Run)) && (prevDequeuedInput.HasFlag(InputNetFlags.Run)); + return dequeuedInput.HasFlag(InputNetFlags.Run) && prevDequeuedInput.HasFlag(InputNetFlags.Run); case InputType.Crouch: - return !(dequeuedInput.HasFlag(InputNetFlags.Crouch)) && (prevDequeuedInput.HasFlag(InputNetFlags.Crouch)); + return dequeuedInput.HasFlag(InputNetFlags.Crouch) && !prevDequeuedInput.HasFlag(InputNetFlags.Crouch); case InputType.Select: return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered case InputType.Deselect: @@ -1139,11 +1242,11 @@ namespace Barotrauma case InputType.Grab: return dequeuedInput.HasFlag(InputNetFlags.Grab); case InputType.Use: - return !(dequeuedInput.HasFlag(InputNetFlags.Use)) && (prevDequeuedInput.HasFlag(InputNetFlags.Use)); + return dequeuedInput.HasFlag(InputNetFlags.Use) && !prevDequeuedInput.HasFlag(InputNetFlags.Use); case InputType.Shoot: - return !(dequeuedInput.HasFlag(InputNetFlags.Shoot)) && (prevDequeuedInput.HasFlag(InputNetFlags.Shoot)); + return dequeuedInput.HasFlag(InputNetFlags.Shoot) && !prevDequeuedInput.HasFlag(InputNetFlags.Shoot); case InputType.Ragdoll: - return !(dequeuedInput.HasFlag(InputNetFlags.Ragdoll)) && (prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll)); + return dequeuedInput.HasFlag(InputNetFlags.Ragdoll) && !prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll); default: return false; } @@ -1264,11 +1367,27 @@ namespace Barotrauma } } } + private List wearableItems = new List(); public float GetSkillLevel(string skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + + if (skillIdentifier != null) + { + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) + { + if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) + { + skillLevel += skillValue; + } + } + } + } + foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) { skillLevel *= affliction.GetSkillMultiplier(); @@ -1422,6 +1541,16 @@ namespace Barotrauma greatestNegativeHealthMultiplier = 1f; } + /// + /// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier + /// + public float StaticHealthMultiplier { get; private set; } = 1; + + public void AddStaticHealthMultiplier(float newMultiplier) + { + StaticHealthMultiplier *= newMultiplier; + } + /// /// Speed reduction from the current limb specific damage. Min 0, max 1. /// @@ -1595,83 +1724,89 @@ namespace Barotrauma } } #endif - if (attackCoolDown > 0.0f) { attackCoolDown -= deltaTime; } - else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient))) + else if (IsKeyDown(InputType.Attack)) { - Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position); - List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); - ignoredBodies.Add(AnimController.Collider.FarseerBody); - - var body = Submarine.PickBody( - SimPosition, - attackPos, - ignoredBodies, - Physics.CollisionCharacter | Physics.CollisionWall); - - IDamageable attackTarget = null; - if (body != null) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { - attackPos = Submarine.LastPickedPosition; - - if (body.UserData is Submarine sub) - { - body = Submarine.PickBody( - SimPosition - ((Submarine)body.UserData).SimPosition, - attackPos - ((Submarine)body.UserData).SimPosition, - ignoredBodies, - Physics.CollisionWall); - - if (body != null) - { - attackPos = Submarine.LastPickedPosition + sub.SimPosition; - attackTarget = body.UserData as IDamageable; - } - } - else - { - if (body.UserData is IDamageable) - { - attackTarget = (IDamageable)body.UserData; - } - else if (body.UserData is Limb) - { - attackTarget = ((Limb)body.UserData).character; - } - } + currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _); } - var currentContexts = GetAttackContexts(); - var validLimbs = AnimController.Limbs.Where(l => + else if (IsPlayer) { - if (l.IsSevered || l.IsStuck) { return false; } - if (l.Disabled) { return false; } - var attack = l.attack; - if (attack == null) { return false; } - if (attack.CoolDownTimer > 0) { return false; } - if (!attack.IsValidContext(currentContexts)) { return false; } - if (attackTarget != null) + Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position); + List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + ignoredBodies.Add(AnimController.Collider.FarseerBody); + + var body = Submarine.PickBody( + SimPosition, + attackPos, + ignoredBodies, + Physics.CollisionCharacter | Physics.CollisionWall); + + IDamageable attackTarget = null; + if (body != null) { - if (!attack.IsValidTarget(attackTarget)) { return false; } - if (attackTarget is ISerializableEntity se && attackTarget is Character) + attackPos = Submarine.LastPickedPosition; + + if (body.UserData is Submarine sub) { - if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + body = Submarine.PickBody( + SimPosition - ((Submarine)body.UserData).SimPosition, + attackPos - ((Submarine)body.UserData).SimPosition, + ignoredBodies, + Physics.CollisionWall); + + if (body != null) + { + attackPos = Submarine.LastPickedPosition + sub.SimPosition; + attackTarget = body.UserData as IDamageable; + } + } + else + { + if (body.UserData is IDamageable) + { + attackTarget = (IDamageable)body.UserData; + } + else if (body.UserData is Limb) + { + attackTarget = ((Limb)body.UserData).character; + } } } - if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } - return true; - }); - var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition)); - // Select closest - var attackLimb = sortedLimbs.FirstOrDefault(); - if (attackLimb != null) - { - attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult); - if (!attackLimb.attack.IsRunning) + var currentContexts = GetAttackContexts(); + var validLimbs = AnimController.Limbs.Where(l => { - attackCoolDown = 1.0f; + if (l.IsSevered || l.IsStuck) { return false; } + if (l.Disabled) { return false; } + var attack = l.attack; + if (attack == null) { return false; } + if (attack.CoolDownTimer > 0) { return false; } + if (!attack.IsValidContext(currentContexts)) { return false; } + if (attackTarget != null) + { + if (!attack.IsValidTarget(attackTarget)) { return false; } + if (attackTarget is ISerializableEntity se && attackTarget is Character) + { + if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + } + } + if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } + return true; + }); + var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition)); + // Select closest + var attackLimb = sortedLimbs.FirstOrDefault(); + if (attackLimb != null) + { + attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult); + if (!attackLimb.attack.IsRunning) + { + attackCoolDown = 1.0f; + } } } } @@ -1746,6 +1881,24 @@ namespace Barotrauma } } + private struct AttackTargetData + { + public Limb AttackLimb { get; set; } + public IDamageable DamageTarget { get; set; } + public Vector2 AttackPos { get; set; } + } + + private AttackTargetData currentAttackTarget; + public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos) + { + currentAttackTarget = new AttackTargetData() + { + AttackLimb = attackLimb, + DamageTarget = damageTarget, + AttackPos = attackPos + }; + } + public bool CanSeeCharacter(Character target) { if (target.Removed) { return false; } @@ -1791,19 +1944,19 @@ namespace Barotrauma return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb; } - public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null) + public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) { - seeingLimb ??= GetSeeingLimb(); - if (seeingLimb == null) { return false; } - ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity; + seeingEntity ??= AnimController.SimplePhysicsEnabled ? this as ISpatialEntity : GetSeeingLimb() as ISpatialEntity; + if (seeingEntity == null) { return false; } + ISpatialEntity sourceEntity = seeingEntity ; // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition); Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside if (target.Submarine == Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); } //we're outside, the other character inside else if (Submarine == null) @@ -1813,7 +1966,7 @@ namespace Barotrauma //both inside different subs else { - closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); + closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff); if (!IsBlocking(closestBody)) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); @@ -1841,52 +1994,44 @@ namespace Barotrauma } } - /// - /// TODO: ensure that works. CheckVisibility takes positions in sim space, but this method uses world positions - /// - public bool CanSeeCharacter(Character target, Vector2 sourceWorldPos) - { - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceWorldPos); - Body closestBody; - if (target.Submarine == null) - { - closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff); - if (closestBody == null) { return true; } - } - else - { - closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff); - if (closestBody == null) { return true; } - } - Structure wall = closestBody.UserData as Structure; - Item item = closestBody.UserData as Item; - Door door = item?.GetComponent(); - return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed); - } - /// /// A simple check if the character Dir is towards the target or not. Uses the world coordinates. /// public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X; - public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); + public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); - public bool HasEquippedItem(Item item) + public bool HasEquippedItem(Item item, InvSlotType? slotType = null) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { - if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; } + if (slotType.HasValue) + { + if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; } + } + else if (Inventory.SlotTypes[i] == InvSlotType.Any) + { + continue; + } + if (Inventory.GetItemAt(i) == item) { return true; } } return false; } - public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true) + public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { - if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } + if (slotType.HasValue) + { + if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; } + } + else if (Inventory.SlotTypes[i] == InvSlotType.Any) + { + continue; + } var item = Inventory.GetItemAt(i); if (item == null) { continue; } if (!allowBroken && item.Condition <= 0.0f) { continue; } @@ -1895,12 +2040,19 @@ namespace Barotrauma return false; } - public Item GetEquippedItem(string tagOrIdentifier) + public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null) { if (Inventory == null) { return null; } for (int i = 0; i < Inventory.Capacity; i++) { - if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } + if (slotType.HasValue) + { + if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; } + } + else if (Inventory.SlotTypes[i] == InvSlotType.Any) + { + continue; + } var item = Inventory.GetItemAt(i); if (item == null) { continue; } if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; } @@ -2022,7 +2174,7 @@ namespace Barotrauma bool hidden = item.HiddenInGame; #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } -#endif +#endif if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } if (item.ParentInventory != null) @@ -2086,7 +2238,10 @@ namespace Barotrauma Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height); // Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect - Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition); + Vector2 playerDistanceCheckPosition = + lowerBodyPosition.Y < upperBodyPosition.Y ? + Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition) : + Vector2.Clamp(itemDisplayRect.Center.ToVector2(), upperBodyPosition, lowerBodyPosition); // If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item if (itemDisplayRect.Contains(playerDistanceCheckPosition)) @@ -2124,7 +2279,7 @@ namespace Barotrauma itemPosition -= Submarine.SimPosition; } var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null && body.UserData as Item != item) { return false; } + if (body != null && body.UserData as Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { return false; } } return true; @@ -2222,6 +2377,10 @@ namespace Barotrauma var item = FindItemAtPosition(mouseSimPos, aimAssist); focusedItem = CanInteract ? item : null; + if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None) + { + FocusedCharacter = null; + } findFocusedTimer = 0.05f; } } @@ -2360,29 +2519,26 @@ namespace Barotrauma { if (!(c is AICharacter) && !c.IsRemotePlayer) continue; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (c.IsPlayer || (c.IsBot && !c.IsDead)) + { + c.Enabled = true; + } + else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { //disable AI characters that are far away from all clients and the host's character and not controlled by anyone - if (c.IsPlayer || (c.IsBot && !c.IsDead)) + float closestPlayerDist = c.GetDistanceToClosestPlayer(); + if (closestPlayerDist > c.Params.DisableDistance) + { + c.Enabled = false; + if (c.IsDead && c.AIController is EnemyAIController) + { + Spawner?.AddToRemoveQueue(c); + } + } + else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) { c.Enabled = true; } - else - { - float closestPlayerDist = c.GetDistanceToClosestPlayer(); - if (closestPlayerDist > c.Params.DisableDistance) - { - c.Enabled = false; - if (c.IsDead && c.AIController is EnemyAIController) - { - Spawner?.AddToRemoveQueue(c); - } - } - else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) - { - c.Enabled = true; - } - } } else if (Submarine.MainSub != null) { @@ -2834,11 +2990,14 @@ namespace Barotrauma Spawner.AddToRemoveQueue(this); } - public void DespawnNow() + public void DespawnNow(bool createNetworkEvents = true) { despawnTimer = GameMain.Config.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true); - Spawner.Update(); + if (createNetworkEvents) + { + Spawner.Update(); + } } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -2899,10 +3058,16 @@ namespace Barotrauma return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this)); } - public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true) + /// Force an order to be set for the character, bypassing hearing checks + public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true, bool force = false) { //set the character order only if the character is close enough to hear the message - if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } + if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } + + if (order.OrderGiver != orderGiver) + { + order.OrderGiver = orderGiver; + } // If there's another character operating the same device, make them dismiss themself if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null) @@ -2911,6 +3076,7 @@ namespace Barotrauma { if (character == this) { continue; } if (character.TeamID != TeamID) { continue; } + if (!(character.AIController is HumanAIController)) { continue; } if (!HumanAIController.IsActive(character)) { continue; } foreach (var currentOrder in character.CurrentOrders) { @@ -2918,7 +3084,7 @@ namespace Barotrauma if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } if (currentOrder.Order.Identifier != order.Identifier) { continue; } if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } - character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character); + character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); break; } } @@ -2936,6 +3102,12 @@ namespace Barotrauma SetOrderProjSpecific(order, orderOption, priority); } + /// Force an order to be set for the character, bypassing hearing checks + public void SetOrder(OrderInfo orderInfo, Character orderGiver, bool speak = true, bool force = false) + { + SetOrder(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, orderGiver, speak: speak, force: force); + } + private void AddCurrentOrder(OrderInfo newOrder) { if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed") @@ -3154,7 +3326,7 @@ namespace Barotrauma /// /// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage. /// - public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null) + public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f) { if (Removed) { @@ -3170,7 +3342,7 @@ namespace Barotrauma var attackResult = targetLimb == null ? AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) : - DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier); + DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration); if (limbHit == null) { return new AttackResult(); } Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; @@ -3302,7 +3474,7 @@ namespace Barotrauma GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true) + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f) { if (Removed) { return new AttackResult(); } @@ -3354,7 +3526,7 @@ namespace Barotrauma } bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); - AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier); + AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration); CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking); if (attacker != this) { @@ -3412,9 +3584,9 @@ namespace Barotrauma /// /// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned. - /// With stunning, the parameter uses a half a second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property. + /// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property. /// - public bool IsKnockedDown => IsDead || IsIncapacitated || CharacterHealth.StunTimer > 0.5f || IsRagdolled; + public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated; public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { @@ -3457,7 +3629,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - statusEffect.GetNearbyTargets(WorldPosition, targets); + targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); statusEffect.Apply(actionType, deltaTime, this, targets); } else @@ -3500,6 +3672,11 @@ namespace Barotrauma // OnDamaged is called only for the limb that is hit. AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); } + //OnActive effects are handled by the afflictions themselves + if (actionType != ActionType.OnActive) + { + CharacterHealth.ApplyAfflictionStatusEffects(actionType); + } } private void Implode(bool isNetworkMessage = false) @@ -3643,10 +3820,7 @@ namespace Barotrauma return; } - if (aiTarget != null) - { - aiTarget.Remove(); - } + aiTarget?.Remove(); aiTarget = new AITarget(this); CharacterHealth.RemoveAllAfflictions(); @@ -3738,8 +3912,9 @@ namespace Barotrauma AnimController.FindHull(worldPos, true); } - public void SaveInventory(Inventory inventory, XElement parentElement) + public static void SaveInventory(Inventory inventory, XElement parentElement) { + if (inventory == null || parentElement == null) { return; } var items = inventory.AllItems.Distinct(); foreach (Item item in items) { @@ -3758,6 +3933,14 @@ namespace Barotrauma } } + /// + /// Calls using 'Inventory' and 'Info.InventoryData' + /// + public void SaveInventory() + { + SaveInventory(Inventory, Info?.InventoryData); + } + public void SpawnInventoryItems(Inventory inventory, XElement itemData) { SpawnInventoryItemsRecursive(inventory, itemData, new List()); @@ -4011,9 +4194,12 @@ namespace Barotrauma public bool IsEngineer => HasJob("engineer"); public bool IsMechanic => HasJob("mechanic"); public bool IsMedic => HasJob("medicaldoctor"); - public bool IsSecurity => HasJob("securityofficer"); + public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer"); public bool IsAssistant => HasJob("assistant"); public bool IsWatchman => HasJob("watchman"); + public bool IsVip => HasJob("prisoner"); + public bool IsPrisoner => HasJob("prisoner"); + public Color? UniqueNameColor { get; set; } = null; public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; @@ -4022,4 +4208,24 @@ namespace Barotrauma return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f); } } + + class ActiveTeamChange + { + public CharacterTeamType DesiredTeamId { get; } + public enum TeamChangePriorities + { + Base, // given to characters when generated or when their base team is set + Willful, // cognitive, willful team changes, such as prisoners escaping + Absolute // possession, insanity, the like + } + public TeamChangePriorities TeamChangePriority { get; } + public bool AggressiveBehavior { get; } + + public ActiveTeamChange(CharacterTeamType desiredTeamId, TeamChangePriorities teamChangePriority, bool aggressiveBehavior = false) + { + DesiredTeamId = desiredTeamId; + TeamChangePriority = teamChangePriority; + AggressiveBehavior = aggressiveBehavior; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index ff478bab3..14e7f34c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -149,6 +149,7 @@ namespace Barotrauma public XElement InventoryData; public XElement HealthData; + public XElement OrderData; private static ushort idCounter; private const string disguiseName = "???"; @@ -455,7 +456,7 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "") { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { @@ -484,8 +485,12 @@ namespace Barotrauma { Name = name; } - else + else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle) { + Name = npcTitle; + } + else + { name = ""; if (CharacterConfigElement.Element("name") != null) { @@ -493,7 +498,7 @@ namespace Barotrauma if (firstNamePath != "") { firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - Name = ToolBox.GetRandomLine(firstNamePath); + Name = ToolBox.GetRandomLine(firstNamePath, randSync); } string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); @@ -501,7 +506,7 @@ namespace Barotrauma { lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); if (Name != "") Name += " "; - Name += ToolBox.GetRandomLine(lastNamePath); + Name += ToolBox.GetRandomLine(lastNamePath, randSync); } } } @@ -1019,7 +1024,273 @@ namespace Barotrauma return charElement; } - public void ApplyHealthData(Character character, XElement healthData) + public static void SaveOrders(XElement parentElement, params OrderInfo[] orders) + { + if (parentElement == null || orders == null || orders.None()) { return; } + // If an order is invalid, we discard the order and increase the priority of the following orders so + // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and + // 2) the order priorities will remain sequential. + int priorityIncrease = 0; + var linkedSubs = GetLinkedSubmarines(); + foreach (var orderInfo in orders) + { + var order = orderInfo.Order; + if (order == null || string.IsNullOrEmpty(order.Identifier)) + { + DebugConsole.ThrowError("Error saving an order - the order or its identifier is null"); + priorityIncrease++; + continue; + } + int? linkedSubIndex = null; + bool targetAvailableInNextLevel = true; + if (order.TargetSpatialEntity != null) + { + var entitySub = order.TargetSpatialEntity.Submarine; + bool isOutside = entitySub == null; + bool canBeOnLinkedSub = !isOutside && Submarine.MainSub != null && entitySub != Submarine.MainSub && linkedSubs.Any(); + bool isOnConnectedLinkedSub = false; + if (canBeOnLinkedSub) + { + for (int i = 0; i < linkedSubs.Count; i++) + { + var ls = linkedSubs[i]; + if (!ls.LoadSub) { continue; } + if (ls.Sub != entitySub) { continue; } + linkedSubIndex = i; + isOnConnectedLinkedSub = Submarine.MainSub.GetConnectedSubs().Contains(entitySub); + break; + } + } + targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); + if (!targetAvailableInNextLevel) + { + if (!order.CanBeGeneralized) + { + DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved."); + priorityIncrease++; + continue; + } + else + { + DebugConsole.Log($"Saving an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order will be saved as a generalized version."); + } + } + } + if (orderInfo.ManualPriority < 1) + { + DebugConsole.ThrowError($"Error saving an order ({order.Identifier}) - the order priority is less than 1"); + priorityIncrease++; + continue; + } + var orderElement = new XElement("order", + new XAttribute("id", order.Identifier), + new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease), + new XAttribute("targettype", (int)order.TargetType)); + if (!string.IsNullOrEmpty(orderInfo.OrderOption)) + { + orderElement.Add(new XAttribute("option", orderInfo.OrderOption)); + } + if (order.OrderGiver != null) + { + orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID)); + } + if (order.TargetSpatialEntity?.Submarine is Submarine targetSub) + { + if (targetSub == Submarine.MainSub) + { + orderElement.Add(new XAttribute("onmainsub", true)); + } + else if(linkedSubIndex.HasValue) + { + orderElement.Add(new XAttribute("linkedsubindex", linkedSubIndex)); + } + } + switch (order.TargetType) + { + case Order.OrderTargetType.Entity when targetAvailableInNextLevel && order.TargetEntity is Entity e: + orderElement.Add(new XAttribute("targetid", (uint)e.ID)); + break; + case Order.OrderTargetType.Position when targetAvailableInNextLevel && order.TargetSpatialEntity is OrderTarget ot: + var orderTargetElement = new XElement("ordertarget"); + var position = ot.WorldPosition; + if (ot.Hull != null) + { + orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID)); + position -= ot.Hull.WorldPosition; + } + orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}")); + orderElement.Add(orderTargetElement); + break; + case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue: + orderElement.Add(new XAttribute("structureid", s.ID)); + orderElement.Add(new XAttribute("wallsectionindex", order.WallSectionIndex.Value)); + break; + } + parentElement.Add(orderElement); + } + } + + /// + /// Save current orders to the parameter element + /// + public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement) + { + var currentOrders = new List(characterInfo.CurrentOrders); + // Sort the current orders to make sure the one with the highest priority comes first + currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + SaveOrders(parentElement, currentOrders.ToArray()); + } + + /// + /// Save current orders to + /// + public void SaveOrderData() + { + OrderData = new XElement("orders"); + SaveOrderData(this, OrderData); + } + + public static void ApplyOrderData(Character character, XElement orderData) + { + if (character == null) { return; } + var orders = LoadOrders(orderData); + foreach (var order in orders) + { + character.SetOrder(order, order.Order?.OrderGiver, speak: false, force: true); + } + } + + public void ApplyOrderData() + { + ApplyOrderData(Character, OrderData); + } + + public static List LoadOrders(XElement ordersElement) + { + var orders = new List(); + if (ordersElement == null) { return orders; } + // If an order is invalid, we discard the order and increase the priority of the following orders so + // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and + // 2) the order priorities will remain sequential. + int priorityIncrease = 0; + var linkedSubs = GetLinkedSubmarines(); + foreach (var orderElement in ordersElement.GetChildElements("order")) + { + Order order = null; + string orderIdentifier = orderElement.GetAttributeString("id", ""); + var orderPrefab = Order.GetPrefab(orderIdentifier); + if (orderPrefab == null) + { + DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\""); + priorityIncrease++; + continue; + } + var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0); + int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1); + var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null; + Entity targetEntity = null; + switch (targetType) + { + case Order.OrderTargetType.Entity: + ushort targetId = (ushort)orderElement.GetAttributeUInt("targetid", Entity.NullEntityID); + if (!GetTargetEntity(targetId, out targetEntity)) { continue; } + var targetComponent = orderPrefab.GetTargetItemComponent(targetEntity as Item); + order = new Order(orderPrefab, targetEntity, targetComponent, orderGiver: orderGiver); + break; + case Order.OrderTargetType.Position: + var orderTargetElement = orderElement.GetChildElement("ordertarget"); + var position = orderTargetElement.GetAttributeVector2("position", Vector2.Zero); + ushort hullId = (ushort)orderTargetElement.GetAttributeUInt("hullid", 0); + if (!GetTargetEntity(hullId, out targetEntity)) { continue; } + if (!(targetEntity is Hull targetPositionHull)) + { + DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull"); + priorityIncrease++; + continue; + } + var orderTarget = new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull); + order = new Order(orderPrefab, orderTarget, orderGiver: orderGiver); + break; + case Order.OrderTargetType.WallSection: + ushort structureId = (ushort)orderElement.GetAttributeInt("structureid", Entity.NullEntityID); + if (!GetTargetEntity(structureId, out targetEntity)) { continue; } + int wallSectionIndex = orderElement.GetAttributeInt("wallsectionindex", 0); + if (!(targetEntity is Structure targetStructure)) + { + DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure"); + priorityIncrease++; + continue; + } + order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver); + break; + } + string orderOption = orderElement.GetAttributeString("option", ""); + int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease; + var orderInfo = new OrderInfo(order, orderOption, manualPriority); + orders.Add(orderInfo); + + bool GetTargetEntity(ushort targetId, out Entity targetEntity) + { + targetEntity = null; + if (targetId == Entity.NullEntityID) { return true; } + Submarine parentSub = null; + if (orderElement.GetAttributeBool("onmainsub", false)) + { + parentSub = Submarine.MainSub; + } + else + { + int linkedSubIndex = orderElement.GetAttributeInt("linkedsubindex", -1); + if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count && + linkedSubs[linkedSubIndex] is LinkedSubmarine linkedSub && linkedSub.LoadSub) + { + parentSub = linkedSub.Sub; + } + } + if (parentSub != null) + { + targetId = GetOffsetId(parentSub, targetId); + targetEntity = Entity.FindEntityByID(targetId); + } + else + { + if (!orderPrefab.CanBeGeneralized) + { + DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order requires a target so it can't be loaded at all."); + priorityIncrease++; + return false; + } + else + { + DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead."); + } + } + return true; + } + } + return orders; + } + + private static List GetLinkedSubmarines() + { + return Entity.GetEntities() + .OfType() + .Where(ls => ls.Submarine == Submarine.MainSub) + .OrderBy(e => e.ID) + .ToList(); + } + + private static ushort GetOffsetId(Submarine parentSub, ushort id) + { + if (parentSub != null) + { + var idRemap = new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset); + return idRemap.GetOffsetId(id); + } + return id; + } + + public static void ApplyHealthData(Character character, XElement healthData) { if (healthData != null) { character?.CharacterHealth.Load(healthData); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 8d7ae53f6..82f03ec56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -14,7 +14,7 @@ namespace Barotrauma public Dictionary SerializableProperties { get; set; } - public float PendingAdditionStrenght { get; set; } + public float PendingAdditionStrength { get; set; } public float AdditionStrength { get; set; } protected float _strength; @@ -32,7 +32,7 @@ namespace Barotrauma float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); if (newValue > _strength) { - PendingAdditionStrenght = Prefab.GrainBurst; + PendingAdditionStrength = Prefab.GrainBurst; } _strength = newValue; } @@ -64,7 +64,7 @@ namespace Barotrauma public Affliction(AfflictionPrefab prefab, float strength) { Prefab = prefab; - PendingAdditionStrenght = Prefab.GrainBurst; + PendingAdditionStrength = Prefab.GrainBurst; _strength = strength; Identifier = prefab?.Identifier; @@ -91,10 +91,12 @@ namespace Barotrauma public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})"; + public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength); + public float GetVitalityDecrease(CharacterHealth characterHealth) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; } @@ -114,7 +116,7 @@ namespace Barotrauma public float GetScreenGrainStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; } @@ -125,7 +127,7 @@ namespace Barotrauma if (Prefab.GrainBurst > 0 && AdditionStrength > amount) { - return AdditionStrength; + return Math.Min(AdditionStrength, 1.0f); } return amount; @@ -134,7 +136,7 @@ namespace Barotrauma public float GetScreenDistortStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; } @@ -147,7 +149,7 @@ namespace Barotrauma public float GetRadialDistortStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; } @@ -160,7 +162,7 @@ namespace Barotrauma public float GetChromaticAberrationStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; } @@ -173,7 +175,7 @@ namespace Barotrauma public float GetScreenBlurStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; } @@ -186,7 +188,7 @@ namespace Barotrauma public float GetSkillMultiplier() { if (Strength < Prefab.ActivationThreshold) { return 1.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 1.0f; } float amount = MathHelper.Lerp( @@ -210,11 +212,11 @@ namespace Barotrauma public float GetResistance(string afflictionId) { - if (Strength < Prefab.ActivationThreshold) return 0.0f; - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 0.0f; - if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f; - if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) return 0.0f; + if (Strength < Prefab.ActivationThreshold) { return 0.0f; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return 0.0f; } + if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) { return 0.0f; } + if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) { return 0.0f; } return MathHelper.Lerp( currentEffect.MinResistance, @@ -224,10 +226,10 @@ namespace Barotrauma public float GetSpeedMultiplier() { - if (Strength < Prefab.ActivationThreshold) return 1.0f; - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 1.0f; - if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) return 1.0f; + if (Strength < Prefab.ActivationThreshold) { return 1.0f; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return 1.0f; } + if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) { return 1.0f; } return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, @@ -250,14 +252,14 @@ namespace Barotrauma { foreach (StatusEffect statusEffect in periodicEffect.StatusEffects) { - ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb); + ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb); PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval); } } } - } + } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return; } if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted @@ -273,7 +275,7 @@ namespace Barotrauma foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { - ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb); + ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb); } float amount = deltaTime; @@ -281,10 +283,10 @@ namespace Barotrauma { amount /= Prefab.GrainBurst; } - if (PendingAdditionStrenght >= 0) + if (PendingAdditionStrength >= 0) { AdditionStrength += amount; - PendingAdditionStrenght -= deltaTime; + PendingAdditionStrength -= deltaTime; } else if (AdditionStrength > 0) { @@ -292,27 +294,37 @@ namespace Barotrauma } } - public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) + public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) + { + var currentEffect = GetActiveEffect(); + if (currentEffect != null) + { + currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb)); + } + } + + private readonly List targets = new List(); + public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb) { statusEffect.SetUser(Source); if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { - statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character); + statusEffect.Apply(type, deltaTime, characterHealth.Character, characterHealth.Character); } if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { - statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb); + statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb); } if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); + statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs); } if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targets); + targets.Clear(); + targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets)); + statusEffect.Apply(type, deltaTime, characterHealth.Character, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 70de29313..ab8ec65ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -69,12 +69,15 @@ namespace Barotrauma if (Strength < DormantThreshold) { DeactivateHusk(); - State = InfectionState.Dormant; + if (Strength > Math.Min(1.0f, DormantThreshold)) + { + State = InfectionState.Dormant; + } } else if (Strength < ActiveThreshold) { DeactivateHusk(); - if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { character.SpeechImpediment = 100; } @@ -128,7 +131,7 @@ namespace Barotrauma character.NeedsAir = false; } - if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { character.SpeechImpediment = 100; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 645fefcea..0606a8368 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -678,7 +678,10 @@ namespace Barotrauma { foreach (Effect effect in effects) { - if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect; + if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) + { + return effect; + } } //if above the strength range of all effects, use the highest strength effect diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 3b36e03a9..730a08b4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -42,7 +42,7 @@ namespace Barotrauma private float GetDiminishMultiplier() { if (Strength < Prefab.ActivationThreshold) { return 1.0f; } - AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 1.0f; } float multiplier = MathHelper.Lerp( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index fd7e4e4ea..ad2d820c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -150,12 +150,9 @@ namespace Barotrauma { max += Character.Info.Job.Prefab.VitalityModifier; } + max *= Character.StaticHealthMultiplier; return max * Character.HealthMultiplier; } - set - { - maxVitality = Math.Max(0, value); - } } public float MinVitality @@ -630,7 +627,10 @@ namespace Barotrauma Kill(); } #if CLIENT - selectedLimbIndex = -1; + if (CharacterHealth.OpenHealthWindow != this) + { + selectedLimbIndex = -1; + } #endif } @@ -833,6 +833,32 @@ namespace Barotrauma #endif } + // We need to use another list of the afflictions when we call the status effects triggered by afflictions, + // because those status effects may add or remove other afflictions while iterating the collection. + private readonly List afflictionsCopy = new List(); + public void ApplyAfflictionStatusEffects(ActionType type) + { + for (int i = 0; i < limbHealths.Count; i++) + { + for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) + { + var affliction = limbHealths[i].Afflictions[j]; + Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i); + if (targetLimb == null) + { + targetLimb = Character.AnimController.MainLimb; + } + affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb); + } + } + afflictionsCopy.Clear(); + afflictionsCopy.AddRange(afflictions); + for (int i = afflictionsCopy.Count - 1; i >= 0; i--) + { + afflictionsCopy[i].ApplyStatusEffects(type, 1.0f, this, targetLimb: null); + } + } + public Pair GetCauseOfDeath() { List currentAfflictions = GetAllAfflictions(true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 6e930be09..b40c60233 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -74,6 +74,20 @@ namespace Barotrauma private string rawAfflictionTypeString; private string[] parsedAfflictionIdentifiers; private string[] parsedAfflictionTypes; + public string[] ParsedAfflictionIdentifiers + { + get + { + return parsedAfflictionIdentifiers; + } + } + public string[] ParsedAfflictionTypes + { + get + { + return parsedAfflictionTypes; + } + } public DamageModifier(XElement element, string parentDebugName) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index c95a4feb2..f87c976ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -90,6 +90,7 @@ namespace Barotrauma public readonly Dictionary ItemSets = new Dictionary(); + public readonly Dictionary CustomNPCSets = new Dictionary(); public HumanPrefab(XElement element, string filePath) { @@ -99,6 +100,7 @@ namespace Barotrauma Job = Job.ToLowerInvariant(); Element = element; element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); + element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1))); PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList(); } @@ -119,11 +121,12 @@ namespace Barotrauma public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) { - npc.CharacterHealth.MaxVitality *= HealthMultiplier; + npc.AddStaticHealthMultiplier(HealthMultiplier); if (GameMain.NetworkMember != null) { - npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer; + npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer); } + var humanAI = npc.AIController as HumanAIController; if (humanAI != null) { @@ -160,18 +163,24 @@ namespace Barotrauma var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync); foreach (XElement itemElement in spawnItems.GetChildElements("item")) { - InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents); + InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents); } } - private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true) + public CharacterInfo GetCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced) + { + var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), randSync); + return characterElement != null ? new CharacterInfo(characterElement) : null; + } + + public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; if (itemPrefab == null) { - DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); return; } Item item = new Item(itemPrefab, character.Position, null); @@ -192,7 +201,11 @@ namespace Barotrauma #endif if (itemElement.GetAttributeBool("equip", false)) { - List allowedSlots = new List(item.AllowedSlots); + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + item.GetComponents().Count() > 1 ? + new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : + new List(item.AllowedSlots); allowedSlots.Remove(InvSlotType.Any); character.Inventory.TryPutItem(item, null, allowedSlots); @@ -215,10 +228,7 @@ namespace Barotrauma } IdCard idCardComponent = item.GetComponent(); - if (idCardComponent != null) - { - idCardComponent.Initialize(character.Info); - } + idCardComponent?.Initialize(character.Info); var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); foreach (string tag in idCardTags) @@ -231,13 +241,10 @@ namespace Barotrauma { wifiComponent.TeamID = character.TeamID; } - if (parentItem != null) - { - parentItem.Combine(item, user: null); - } + parentItem?.Combine(item, user: null); foreach (XElement childItemElement in itemElement.Elements()) { - InitializeItems(character, childItemElement, submarine, item, createNetworkEvents); + InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 21b7f6cb2..d8cd67162 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -158,9 +158,12 @@ namespace Barotrauma if (itemElement.GetAttributeBool("equip", false)) { - List allowedSlots = new List(item.AllowedSlots); + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + item.GetComponents().Count() > 1 ? + new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : + new List(item.AllowedSlots); allowedSlots.Remove(InvSlotType.Any); - character.Inventory.TryPutItem(item, null, allowedSlots); } else @@ -201,10 +204,7 @@ namespace Barotrauma item.AddTag("job:" + Name); IdCard idCardComponent = item.GetComponent(); - if (idCardComponent != null) - { - idCardComponent.Initialize(character.Info); - } + idCardComponent?.Initialize(character.Info); } foreach (WifiComponent wifiComponent in item.GetComponents()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index ab7c508c4..c918b7363 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -1,9 +1,9 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Xml.Linq; -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 { @@ -43,6 +43,12 @@ namespace Barotrauma Prefabs.Remove(this); } + private static readonly Dictionary _itemRepairPriorities = new Dictionary(); + /// + /// Tag -> priority. + /// + public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; + public static XElement NoJobElement; public static JobPrefab Get(string identifier) { @@ -178,6 +184,14 @@ namespace Barotrauma private set; } + //whether the job should be available to NPCs + [Serialize(false, false)] + public bool HiddenJob + { + get; + private set; + } + public Sprite Icon; public Sprite IconSmall; @@ -195,7 +209,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); Name = TextManager.Get("JobName." + Identifier); - Description = TextManager.Get("JobDescription." + Identifier); + Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty; Identifier = Identifier.ToLowerInvariant(); Element = element; @@ -265,7 +279,7 @@ namespace Barotrauma } - public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => p.Identifier != "watchman", sync); + public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync); public static void LoadAll(IEnumerable files) { @@ -286,7 +300,6 @@ namespace Barotrauma } foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; } if (element.IsOverride()) { var job = new JobPrefab(element.FirstElement(), file.Path) @@ -297,6 +310,7 @@ namespace Barotrauma } else { + if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; } var job = new JobPrefab(element, file.Path) { ContentPackage = file.ContentPackage @@ -304,8 +318,31 @@ namespace Barotrauma Prefabs.Add(job, false); } } - NoJobElement = NoJobElement ?? mainElement.Element("NoJob"); - NoJobElement = NoJobElement ?? mainElement.Element("nojob"); + NoJobElement ??= mainElement.GetChildElement("nojob"); + var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities"); + if (itemRepairPrioritiesElement != null) + { + foreach (var subElement in itemRepairPrioritiesElement.Elements()) + { + string tag = subElement.GetAttributeString("tag", null); + if (tag != null) + { + float priority = subElement.GetAttributeFloat("priority", -1f); + if (priority >= 0) + { + _itemRepairPriorities.TryAdd(tag, priority); + } + else + { + DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); + } + } + else + { + DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); + } + } + } } public static void RemoveByFile(string filePath) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index c1e9394a4..92087ad79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -683,7 +683,7 @@ namespace Barotrauma private readonly List appliedDamageModifiers = new List(); private readonly List tempModifiers = new List(); private readonly List afflictionsCopy = new List(); - public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1) + public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f) { appliedDamageModifiers.Clear(); afflictionsCopy.Clear(); @@ -726,7 +726,12 @@ namespace Barotrauma float finalDamageModifier = damageMultiplier; foreach (DamageModifier damageModifier in tempModifiers) { - finalDamageModifier *= damageModifier.DamageMultiplier; + float damageModifierValue = damageModifier.DamageMultiplier; + if (damageModifier.DeflectProjectiles && damageModifierValue < 1f) + { + damageModifierValue = MathHelper.Lerp(damageModifierValue, 1f, penetration); + } + finalDamageModifier *= damageModifierValue; } if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f)) { @@ -832,10 +837,7 @@ namespace Barotrauma } } - if (attack != null) - { - attack.UpdateCoolDown(deltaTime); - } + attack?.UpdateCoolDown(deltaTime); } private float reEnableTimer = -1; @@ -981,13 +983,16 @@ namespace Barotrauma NetEntityEvent.Type.ExecuteAttack, this, (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0 + damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0, + attackSimPos.X, + attackSimPos.Y }); #endif } Vector2 diff = attackSimPos - SimPosition; bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning; + if (applyForces) { if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0) @@ -1143,7 +1148,7 @@ namespace Barotrauma statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); - statusEffect.GetNearbyTargets(WorldPosition, targets); + targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets)); statusEffect.Apply(actionType, deltaTime, character, targets); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index d8aa2419a..40e20c196 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -526,7 +526,7 @@ namespace Barotrauma [Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] public bool AttackWhenProvoked { get; private set; } - [Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] + [Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } [Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] @@ -547,8 +547,8 @@ namespace Barotrauma [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] public bool RandomAttack { get; private set; } - [Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids."), Editable] - public bool Infiltrate { get; private set; } + [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable] + public bool CanOpenDoors { get; private set; } [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] public bool AvoidAbyss { get; set; } @@ -565,6 +565,9 @@ namespace Barotrauma [Serialize(0f, true, description: ""), Editable] public float AggressionCumulation { get; private set; } + [Serialize(WallTargetingMethod.Target, true, description: ""), Editable] + public WallTargetingMethod WallTargetingMethod { get; private set; } + public IEnumerable Targets => targets; protected readonly List targets = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 305baebb7..dbfb4cd19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -50,7 +50,8 @@ namespace Barotrauma Corpses, WreckAIConfig, UpgradeModules, - MapCreature + MapCreature, + EnemySubmarine } public class ContentPackage @@ -101,7 +102,8 @@ namespace Barotrauma ContentType.Orders, ContentType.Corpses, ContentType.UpgradeModules, - ContentType.MapCreature + ContentType.MapCreature, + ContentType.EnemySubmarine }; //at least one file of each these types is required in core content packages @@ -132,7 +134,8 @@ namespace Barotrauma ContentType.EventManagerSettings, ContentType.Orders, ContentType.Corpses, - ContentType.UpgradeModules + ContentType.UpgradeModules, + ContentType.EnemySubmarine }; public static IEnumerable CorePackageRequiredFiles @@ -191,13 +194,13 @@ namespace Barotrauma isCorePackage = value; if (isCorePackage && regularPackages.Contains(this)) { - corePackages.Add(this); - regularPackages.Remove(this); + corePackages.AddOnMainThread(this); + regularPackages.RemoveOnMainThread(this); } else if (!isCorePackage && corePackages.Contains(this)) { - regularPackages.Add(this); - corePackages.Remove(this); + regularPackages.AddOnMainThread(this); + corePackages.RemoveOnMainThread(this); } } } @@ -212,7 +215,6 @@ namespace Barotrauma private readonly List filesToAdd; private readonly List filesToRemove; - public IReadOnlyList Files { get { return files; } @@ -409,6 +411,7 @@ namespace Barotrauma case ContentType.Submarine: case ContentType.Wreck: case ContentType.BeaconStation: + case ContentType.EnemySubmarine: break; default: try @@ -526,7 +529,7 @@ namespace Barotrauma { refreshFiles = true; } - corePackages.Remove(p); + corePackages.RemoveOnMainThread(p); } else { @@ -534,16 +537,16 @@ namespace Barotrauma { refreshFiles = true; } - regularPackages.Remove(p); + regularPackages.RemoveOnMainThread(p); } } if (IsCorePackage) { - corePackages.Add(this); + corePackages.AddOnMainThread(this); } else { - regularPackages.Add(this); + regularPackages.AddOnMainThread(this); } if (refreshFiles) @@ -609,7 +612,8 @@ namespace Barotrauma catch (Exception e) { - DebugConsole.ThrowError("Error while calculating content package hash: ", e); + DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e); + break; } } @@ -740,18 +744,18 @@ namespace Barotrauma } if (newPackage.IsCorePackage) { - corePackages.Add(newPackage); + corePackages.AddOnMainThread(newPackage); } else { - regularPackages.Add(newPackage); + regularPackages.AddOnMainThread(newPackage); } } public static void RemovePackage(ContentPackage package) { - if (package.IsCorePackage) { corePackages.Remove(package); } - else { regularPackages.Remove(package); } + if (package.IsCorePackage) { corePackages.RemoveOnMainThread(package); } + else { regularPackages.RemoveOnMainThread(package); } } public static void LoadAll() @@ -772,9 +776,9 @@ namespace Barotrauma IEnumerable files = Directory.GetFiles(folder, "*.xml"); - corePackages.Clear(); + corePackages.ClearOnMainThread(); var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList(); - regularPackages.Clear(); + regularPackages.ClearOnMainThread(); foreach (string filePath in files) { @@ -812,7 +816,7 @@ namespace Barotrauma .OrderBy(p => order(p)) .ThenBy(p => regularPackages.IndexOf(p)) .ToList(); - regularPackages.Clear(); regularPackages.AddRange(ordered); + regularPackages.ClearOnMainThread(); regularPackages.AddRangeOnMainThread(ordered); (config ?? GameMain.Config)?.SortContentPackages(refreshAll); } @@ -822,12 +826,12 @@ namespace Barotrauma { if (IsCorePackage) { - corePackages.Remove(this); + corePackages.RemoveOnMainThread(this); if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); } } else { - regularPackages.Remove(this); + regularPackages.RemoveOnMainThread(this); if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); } } GameMain.Config.SaveNewPlayerConfig(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 36daaf126..acaa5db14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -196,7 +196,7 @@ namespace Barotrauma UpdaterUtil.SaveFileList("filelist.xml"); })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, + commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { List characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList(); @@ -652,15 +652,20 @@ namespace Barotrauma }; }, isCheat: true)); - commands.Add(new Command("heal", "heal [character name]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed.", (string[] args) => + commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) => { - Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args); + bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase); + Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args); if (healedCharacter != null) { healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f); healedCharacter.Oxygen = 100.0f; healedCharacter.Bloodloss = 0.0f; healedCharacter.SetStun(0.0f, true); + if (healAll) + { + healedCharacter.CharacterHealth.RemoveAllAfflictions(); + } } }, () => @@ -1449,6 +1454,20 @@ namespace Barotrauma return new[] { primaries, identifiers }; })); + commands.Add(new Command("setdifficulty|forcedifficulty", "difficulty [0-100]. Leave the parameter empty to disable.", (string[] args) => + { + if (args.Length == 0) + { + Level.ForcedDifficulty = null; + NewMessage($"Forced difficulty level disabled.", Color.Green); + } + else if (float.TryParse(args[0], out float difficulty)) + { + Level.ForcedDifficulty = difficulty; + NewMessage($"Set the difficulty level to { Level.ForcedDifficulty }.", Color.Yellow); + } + }, isCheat: true)); + commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null)); commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) => @@ -1592,7 +1611,7 @@ namespace Barotrauma commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => { return new string[][] { ListCharacterNames() }; - })); + }, isCheat: true)); commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true)); 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)); @@ -1886,6 +1905,18 @@ namespace Barotrauma } if (string.IsNullOrWhiteSpace(args[0])) { return; } + CharacterTeamType teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; + if (args.Length > 2) + { + try + { + teamType = (CharacterTeamType)int.Parse(args[2]); + } + catch + { + DebugConsole.ThrowError($"\"{args[2]}\" is not a valid team id."); + } + } if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; } @@ -1896,8 +1927,7 @@ namespace Barotrauma spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); if (GameMain.GameSession != null) { - //TODO: a way to select which team to spawn to? - spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; + spawnedCharacter.TeamID = teamType; #if CLIENT GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index 1b8fd0967..fd60cddb2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -23,7 +23,17 @@ namespace Barotrauma protected PropertyConditional.OperatorType Operator { get; set; } - public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + { + if (string.IsNullOrEmpty(Condition)) + { + Condition = element.GetAttributeString("value", string.Empty); + if (string.IsNullOrEmpty(Condition)) + { + DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element})."); + } + } + } protected override bool? DetermineSuccess() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 3e12045f1..9f53430a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -179,7 +179,7 @@ namespace Barotrauma { if (speaker == null) { return; } speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.ActiveConversation = this; + speaker.ActiveConversation = null; speaker.SetCustomInteract(null, null); #if SERVER GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); @@ -213,7 +213,14 @@ namespace Barotrauma if (dialogOpened) { #if CLIENT - Character.DisableControls = true; + if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction")) + { + Character.DisableControls = true; + } + else + { + Reset(); + } #endif if (ShouldInterrupt()) { @@ -240,7 +247,7 @@ namespace Barotrauma { TryStartConversation(speaker); } - else + else if (speaker.ActiveConversation != this) { speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; speaker.ActiveConversation = this; @@ -303,9 +310,15 @@ namespace Barotrauma private bool IsValidTarget(Entity e) { - return - e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && + bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated && (e == Character.Controlled || character.IsRemotePlayer); +#if SERVER + UpdateIgnoredClients(); + isValid &= !ignoredClients.Keys.Any(c => c.Character == e); +#elif CLIENT + isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen); +#endif + return isValid; } private void TryStartConversation(Character speaker, Character targetCharacter = null) @@ -348,10 +361,18 @@ namespace Barotrauma { ParentEvent.AddTarget(InvokerTag, targetCharacter); } - + ShowDialog(speaker, targetCharacter); dialogOpened = true; + if (speaker != null) + { + speaker.CampaignInteractionType = CampaignMode.InteractionType.None; + speaker.SetCustomInteract(null, null); +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); +#endif + } } partial void ShowDialog(Character speaker, Character targetCharacter); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 9fc395d0a..301ca0f74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -14,6 +14,18 @@ namespace Barotrauma [Serialize("", true)] public string MissionTag { get; set; } + [Serialize("", true, 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(0, true, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] + public int MinLocationDistance { get; set; } + + [Serialize(true, true, description: "If true, the mission has to be unlocked in a location further on the campaign map.")] + public bool UnlockFurtherOnMap { get; set; } + + [Serialize(false, true, description: "If true, a suitable location is forced on the map if one isn't found.")] + public bool CreateLocationIfNotFound { get; set; } + private bool isFinished; public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) @@ -44,33 +56,82 @@ namespace Barotrauma if (GameMain.GameSession.GameMode is CampaignMode campaign) { MissionPrefab prefab = null; - if (!string.IsNullOrEmpty(MissionIdentifier)) + var unlockLocation = FindUnlockLocation(); + if (unlockLocation == null && CreateLocationIfNotFound) { - prefab = campaign.Map.CurrentLocation.UnlockMissionByIdentifier(MissionIdentifier); - } - else if (!string.IsNullOrEmpty(MissionTag)) - { - prefab = campaign.Map.CurrentLocation.UnlockMissionByTag(MissionTag); - } - if (campaign is MultiPlayerCampaign mpCampaign) - { - mpCampaign.LastUpdateID++; + //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()); + if (emptyLocation != null) + { + emptyLocation.ChangeType(Barotrauma.LocationType.List.Find(lt => lt.Identifier.Equals(LocationType, StringComparison.OrdinalIgnoreCase))); + unlockLocation = emptyLocation; + } } - if (prefab != null) + if (unlockLocation != null) { -#if CLIENT - new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + if (!string.IsNullOrEmpty(MissionIdentifier)) { - IconColor = prefab.IconColor - }; -#else - NotifyMissionUnlock(prefab); -#endif + prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); + } + else if (!string.IsNullOrEmpty(MissionTag)) + { + prefab = unlockLocation.UnlockMissionByTag(MissionTag); + } + if (campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.LastUpdateID++; + } + if (prefab != null) + { + DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\"."); + #if CLIENT + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), + new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + { + IconColor = prefab.IconColor + }; + #else + NotifyMissionUnlock(prefab); + #endif + } + } + else + { + DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})"); } } - isFinished = true; + isFinished = true; + } + + private Location FindUnlockLocation() + { + var campaign = GameMain.GameSession.GameMode as CampaignMode; + if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1) + { + return campaign.Map.CurrentLocation; + } + + return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet()); + } + + private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) + { + var campaign = GameMain.GameSession.GameMode as CampaignMode; + if (currLocation.Type.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase) && currDistance >= MinLocationDistance && + (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) + { + return currLocation; + } + checkedLocations.Add(currLocation); + foreach (LocationConnection connection in currLocation.Connections) + { + var otherLocation = connection.OtherLocation(currLocation); + if (checkedLocations.Contains(otherLocation)) { continue; } + var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations); + if (unlockLocation != null) { return unlockLocation; } + } + return null; } public override string ToDebugString() @@ -84,8 +145,8 @@ namespace Barotrauma foreach (Client client in GameMain.Server.ConnectedClients) { IWriteMessage outmsg = new WriteOnlyMessage(); - outmsg.Write((byte) ServerPacketHeader.EVENTACTION); - outmsg.Write((byte) EventManager.NetworkEventType.MISSION); + outmsg.Write((byte)ServerPacketHeader.EVENTACTION); + outmsg.Write((byte)EventManager.NetworkEventType.MISSION); outmsg.Write(prefab.Identifier); GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs new file mode 100644 index 000000000..f72877974 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -0,0 +1,73 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class NPCChangeTeamAction : EventAction + { + [Serialize("", true)] + public string NPCTag { get; set; } + + [Serialize(0, true)] + public int TeamTag { get; set; } + + [Serialize(false, true)] + public bool AddToCrew { get; set; } + + private bool isFinished = false; + + public NPCChangeTeamAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + private List affectedNpcs = null; + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList(); + foreach (var npc in affectedNpcs) + { + CharacterTeamType newTeam = (CharacterTeamType)TeamTag; + // characters will still remain on friendlyNPC team for rest of the tick + npc.SetOriginalTeam(newTeam); + + if (AddToCrew && (newTeam == CharacterTeamType.Team1 || newTeam == CharacterTeamType.Team2)) + { + npc.Info.StartItemsGiven = true; + + GameMain.GameSession.CrewManager.AddCharacter(npc); + foreach (Item item in npc.Inventory.AllItems) + { + item.AllowStealing = true; + var wifiComponent = item.GetComponent(); + if (wifiComponent != null) + { + wifiComponent.TeamID = newTeam; + } + } +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, newTeam, npc.Inventory.AllItems.Select(it => it.ID).ToArray() }); +#endif + } + } + isFinished = true; + } + + public override bool IsFinished(ref string goTo) + { + return isFinished; + } + + public override void Reset() + { + isFinished = false; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCChangeTeamAction)} -> (NPCTag: {NPCTag.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 1eb6200b1..eaa494848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -48,9 +48,9 @@ namespace Barotrauma } else { - foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) + foreach (var objective in humanAiController.ObjectiveManager.Objectives) { - if (goToObjective.Target == target) + if (objective is AIObjectiveGoTo goToObjective && goToObjective.Target == target) { goToObjective.Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 716427172..2d1e1e4d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -20,11 +20,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(ItemIdentifier)) { - ItemIdentifier = element.GetAttributeString("itemidentifiers", ""); - } - if (string.IsNullOrWhiteSpace(ItemIdentifier)) - { - DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - RemoveItemAction without an item identifier."); + ItemIdentifier = element.GetAttributeString("itemidentifiers", null) ?? element.GetAttributeString("identifier", ""); } } @@ -47,7 +43,7 @@ namespace Barotrauma bool hasValidTargets = false; foreach (Entity target in targets) { - if (target is Character character && character.Inventory != null) + if (target is Character character && character.Inventory != null || target is Item) { hasValidTargets = true; break; @@ -55,20 +51,31 @@ namespace Barotrauma } if (!hasValidTargets) { return; } - List usedItems = new List(); + HashSet removedItems = new HashSet(); foreach (Entity target in targets) { Inventory inventory = (target as Character)?.Inventory; - if (inventory == null) { continue; } - while (usedItems.Count < Amount) + if (inventory != null) { - var item = inventory.FindItem(it => - it != null && - !usedItems.Contains(it) && - it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true); - if (item == null) { break; } - Entity.Spawner.AddToRemoveQueue(item); - usedItems.Add(item); + while (removedItems.Count < Amount) + { + var item = inventory.FindItem(it => + it != null && + !removedItems.Contains(it) && + (string.IsNullOrEmpty(ItemIdentifier) || it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)), recursive: true); + if (item == null) { break; } + Entity.Spawner.AddToRemoveQueue(item); + removedItems.Add(item); + } + } + else if (target is Item item) + { + if (string.IsNullOrEmpty(ItemIdentifier) || item.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)) + { + Entity.Spawner.AddToRemoveQueue(item); + removedItems.Add(item); + if (removedItems.Count >= Amount) { break; } + } } } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 66b9f360a..4737eba63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -15,7 +15,8 @@ namespace Barotrauma Outpost, MainPath, Ruin, - Wreck + Wreck, + BeaconStation } [Serialize("", true, description: "Species name of the character to spawn.")] @@ -102,31 +103,36 @@ namespace Barotrauma public override void Update(float deltaTime) { if (spawned) { return; } - + if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier)) { HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); - ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => + if (humanPrefab != null) { - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); - if (LootingIsStealing) + ISpatialEntity spawnPos = GetSpawnPos(); + Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.AllItems) + if (newCharacter == null) { return; } + newCharacter.Prefab = humanPrefab; + newCharacter.TeamID = CharacterTeamType.FriendlyNPC; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + if (LootingIsStealing) { - item.SpawnedInOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.AllItems) + { + item.SpawnedInOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } } else if (!string.IsNullOrEmpty(SpeciesName)) { @@ -196,8 +202,7 @@ namespace Barotrauma } } - spawned = true; - + spawned = true; } public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount) @@ -225,6 +230,7 @@ namespace Barotrauma SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost), SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck), SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null), + SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; @@ -250,15 +256,15 @@ namespace Barotrauma SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck), SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null), + SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); - var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false).ToList(); if (moduleFlags != null && moduleFlags.Any()) { - List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Any(moduleFlags.Contains) ?? false).ToList(); + List spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList(); if (spawnPoints.Any()) { potentialSpawnPoints = spawnPoints; @@ -267,8 +273,10 @@ namespace Barotrauma if (spawnpointTags != null && spawnpointTags.Any()) { - var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag))) - .Where(wp => wp.ConnectedDoor == null && !wp.isObstructed); + var spawnPoints = potentialSpawnPoints + .Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag))) + .Where(wp => wp.ConnectedDoor == null && !wp.isObstructed); + if (spawnPoints.Any()) { potentialSpawnPoints = spawnPoints.ToList(); @@ -293,6 +301,7 @@ namespace Barotrauma } //don't spawn in an airlock module if there are other options + var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false); if (airlockSpawnPoints.Count() < validSpawnPoints.Count()) { validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 91244267e..8a3630247 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -51,7 +51,14 @@ namespace Barotrauma { foreach (var target in targets) { - effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + if (target is Item targetItem) + { + effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects); + } + else + { + effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity); + } } } #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index ef22c7638..c96723441 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class TagAction : EventAction { - public enum SubType { Any= 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } + public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } [Serialize("", true)] public string Criteria { get; set; } @@ -67,6 +67,16 @@ namespace Barotrauma #endif } + private void TagHumansByIdentifier(string identifier) + { + foreach (Character c in Character.CharacterList) + { + if (c.Prefab?.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase) ?? false) + { + ParentEvent.AddTarget(Tag, c); + } + } + } private void TagStructuresByIdentifier(string identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); @@ -122,6 +132,9 @@ namespace Barotrauma case "crew": TagCrew(); break; + case "humanprefabidentifier": + if (kvp.Length > 1) { TagHumansByIdentifier(kvp[1].Trim()); } + break; case "structureidentifier": if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index ceb0da293..e4855e9a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -1,3 +1,4 @@ +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Linq; using System.Xml.Linq; @@ -30,6 +31,9 @@ namespace Barotrauma [Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")] public bool DisableIfTargetIncapacitated { get; set; } + [Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")] + public bool WaitForInteraction { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) @@ -44,12 +48,15 @@ namespace Barotrauma } public override void Reset() { + ResetTargetIcons(); isRunning = false; isFinished = false; } public bool isRunning = false; + private Either npcOrItem = null; + public override void Update(float deltaTime) { if (isFinished) { return; } @@ -81,20 +88,104 @@ namespace Barotrauma if (DisableInCombat && IsInCombat(e2)) { continue; } if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; } - 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 (WaitForInteraction) { - Trigger(e1, e2); - return; + Character player = null; + Character npc = null; + Item item = null; + npcOrItem?.TryGet(out npc); + npcOrItem?.TryGet(out item); + if (e1 is Character char1) + { + if (char1.IsBot) { npc ??= char1; } + else { player = char1; } + } + else + { + item ??= e1 as Item; + } + if (e2 is Character char2) + { + if (char2.IsBot) { npc ??= char2; } + else { player = char2; } + } + else + { + item ??= e2 as Item; + } + + if (player != null) + { + if (npc != null) + { + if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine) + { + npcOrItem = npc; + npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; + npc.RequireConsciousnessForCustomInteract = false; +#if CLIENT + npc.SetCustomInteract( + (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, + TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use))); +#else + npc.SetCustomInteract( + (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, + TextManager.Get("CampaignInteraction.Talk")); + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); +#endif + } + + return; + } + else if (item != null) + { + npcOrItem = item; + item.CampaignInteractionType = CampaignMode.InteractionType.Examine; + if (player.SelectedConstruction == item || + player.Inventory.Contains(item) || + (player.FocusedItem == item && player.IsKeyHit(InputType.Use))) + { + Trigger(e1, e2); + return; + } + } + } + } + else + { + 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) + { + Trigger(e1, e2); + return; + } } } } } + private void ResetTargetIcons() + { + if (npcOrItem == null) { return; } + if (npcOrItem.TryGet(out Character npc)) + { + npc.CampaignInteractionType = CampaignMode.InteractionType.None; + npc.SetCustomInteract(null, null); + npc.RequireConsciousnessForCustomInteract = true; +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); +#endif + } + else if (npcOrItem.TryGet(out Item item)) + { + item.CampaignInteractionType = CampaignMode.InteractionType.None; + } + } + private bool IsCloseEnoughToHull(Entity e, out Hull hull) { hull = null; @@ -157,6 +248,7 @@ namespace Barotrauma private void Trigger(Entity entity1, Entity entity2) { + ResetTargetIcons(); if (!string.IsNullOrEmpty(ApplyToTarget1)) { ParentEvent.AddTarget(ApplyToTarget1, entity1); @@ -174,7 +266,14 @@ namespace Barotrauma { if (string.IsNullOrEmpty(TargetModuleType)) { - return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})"; + return + $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" + + (WaitForInteraction ? + $"Selected non-player target: {(npcOrItem?.ToString() ?? "").ColorizeObject()}, " : + $"Distance: {((int)distance).ColorizeObject()}, ") + + $"Radius: {Radius.ColorizeObject()}, " + + $"TargetTags: {Target1Tag.ColorizeObject()}, " + + $"{Target2Tag.ColorizeObject()})"; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 32b7481af..3ede84eeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -50,7 +50,7 @@ namespace Barotrauma private float calculateDistanceTraveledTimer; private float distanceTraveled; - private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger; + private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength; private float roundDuration; @@ -107,7 +107,7 @@ namespace Barotrauma totalPathLength = 0.0f; if (level != null) { - var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition)); + var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition)); totalPathLength = steeringPath.TotalLength; } @@ -124,7 +124,7 @@ namespace Barotrauma } MTRandom rand = new MTRandom(seed); - var initialEventSet = SelectRandomEvents(EventSet.List); + var initialEventSet = SelectRandomEvents(EventSet.List, rand); if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); @@ -168,7 +168,7 @@ namespace Barotrauma if (eventSet == null) { return; } if (eventSet.OncePerOutpost) { - foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) + foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.prefab)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) { @@ -374,11 +374,11 @@ namespace Barotrauma preloadedSprites.Clear(); } - private float CalculateCommonness(Pair eventPrefab) + private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { - if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; } - float retVal = eventPrefab.Second; - if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; } + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } + float retVal = baseCommonness; + if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; } return retVal; } @@ -386,21 +386,25 @@ namespace Barotrauma { if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } - +#if DEBUG + DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue); +#else + DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}"); +#endif int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) { - applyCount = Level.Loaded.Ruins.Count(); - foreach (var ruin in Level.Loaded.Ruins) + applyCount = level.Ruins.Count(); + foreach (var ruin in level.Ruins) { spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; }); } } else if (eventSet.PerCave) { - applyCount = Level.Loaded.Caves.Count(); - foreach (var cave in Level.Loaded.Caves) + applyCount = level.Caves.Count(); + foreach (var cave in level.Caves) { spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; }); } @@ -416,49 +420,62 @@ namespace Barotrauma } var suitablePrefabs = eventSet.EventPrefabs.FindAll(e => - string.IsNullOrEmpty(e.First.BiomeIdentifier) || - e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); + string.IsNullOrEmpty(e.prefab.BiomeIdentifier) || + e.prefab.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); + for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { if (suitablePrefabs.Count > 0) { - List> unusedEvents = new List>(suitablePrefabs); + var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(suitablePrefabs); for (int j = 0; j < eventSet.EventCount; j++) { - if (unusedEvents.All(e => CalculateCommonness(e) <= 0.0f)) { break; } - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand); - if (eventPrefab != null) + if (unusedEvents.All(e => CalculateCommonness(e.prefab, e.commonness) <= 0.0f)) { break; } + (EventPrefab eventPrefab, float commonness, float probability) = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e.prefab, e.commonness)).ToList(), rand); + if (eventPrefab != null && rand.NextDouble() <= probability) { - var newEvent = eventPrefab.First.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } - DebugConsole.Log("Initialized event " + newEvent.ToString()); +#if DEBUG + DebugConsole.NewMessage($"Initialized event {newEvent}"); +#else + DebugConsole.Log($"Initialized event {newEvent}"); +#endif if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); } selectedEvents[eventSet].Add(newEvent); - unusedEvents.Remove(eventPrefab); + unusedEvents.Remove((eventPrefab, commonness, probability)); } } } if (eventSet.ChildSets.Count > 0) { - var newEventSet = SelectRandomEvents(eventSet.ChildSets); - if (newEventSet != null) { CreateEvents(newEventSet, rand); } + var newEventSet = SelectRandomEvents(eventSet.ChildSets, rand); + if (newEventSet != null) + { + CreateEvents(newEventSet, rand); + } } } else { - foreach (Pair eventPrefab in suitablePrefabs) + foreach ((EventPrefab eventPrefab, float commonness, float probability) in suitablePrefabs) { - var newEvent = eventPrefab.First.CreateInstance(); + if (rand.NextDouble() > probability) { continue; } + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); - DebugConsole.Log("Initialized event " + newEvent.ToString()); +#if DEBUG + DebugConsole.NewMessage($"Initialized event {newEvent}"); +#else + DebugConsole.Log($"Initialized event {newEvent}"); +#endif if (!selectedEvents.ContainsKey(eventSet)) { selectedEvents.Add(eventSet, new List()); @@ -474,10 +491,10 @@ namespace Barotrauma } } - private EventSet SelectRandomEvents(List eventSets) + private EventSet SelectRandomEvents(List eventSets, Random random = null) { if (level == null) { return null; } - MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); + Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = eventSets.Where(es => @@ -496,7 +513,8 @@ namespace Barotrauma } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); - float randomNumber = (float)rand.NextDouble() * totalCommonness; + float randomNumber = (float)rand.NextDouble(); + randomNumber *= totalCommonness; foreach (EventSet eventSet in allowedEventSets) { float commonness = eventSet.GetCommonness(level); @@ -694,47 +712,103 @@ namespace Barotrauma // enemy amount -------------------------------------------------------- enemyDanger = 0.0f; + monsterTotalStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } if (!(character.AIController is EnemyAIController enemyAI)) { continue; } + if (!enemyAI.AIParams.StayInAbyss) + { + // Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub. + monsterTotalStrength += enemyAI.CombatStrength; + } + + // Example combat strengths: + // Hammerheadspawn 1 + // Moloch Pupa 1 + // Terminal cell 20 + // Leucocyte 40 + // Husk 90 + // Crawler 100 + // Unarmored Mudraptor 140 + // Spineling 150 + // Tigerthresher 200 + // Armored Mudraptor 210 + // Watcher 400 + // Golden Hammerhead 400 + // Hammerhead 500 + // Hammerhead Matriarch 550 + // Bonethresher 600 + // Moloch 1250 + // Black Moloch 1500 + // Endworm 10000 if (character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { - //crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f - enemyDanger += enemyAI.CombatStrength / 100.0f; + // Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42 + enemyDanger += enemyAI.CombatStrength / 500.0f; } else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null) { - //enemy outside and targeting the sub or something in it - //moloch adds 0.24 to enemy danger, a crawler 0.02 - enemyDanger += enemyAI.CombatStrength / 1000.0f; + // Enemy outside targeting the sub or something in it + // -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25. + enemyDanger += enemyAI.CombatStrength / 5000.0f; } } + // Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub. + // On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4. + // And if they get inside, we add 0.1 per crawler on that. + // So, in practice the danger per enemy that is attacking the sub is half of what it would be when the enemy is not targeting the sub. + // 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside). + // 5 Mudraptors -> +0.21 (0.42 in total, before they get inside). + // 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub). + // 2 Molochs -> +0.5 (1.0 in total, if both target the sub). + enemyDanger += monsterTotalStrength / 5000f; enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f); + // The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle. + // Some examples that result in the max intensity even when the creatures would just idle around. + // The values are theoretical, because in practice many of the monsters are targeting the sub, which will double the danger of those monster and effectively halve the max monster count. + // In practice we don't use the max intensity. For example on level 50 we use max intensity 50, which would mean that we'd halve the numbers below. + // There's no hard cap for the monster count, but if the amount of monsters is higher than this, we don't spawn more monsters from the events: + // 50 Crawlers (We shouldn't actually ever spawn that many. 12 is the max per event, but theoretically 25 crawlers would result in max intensity). + // 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity). + // 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity). + // 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity). + // hull status (gaps, flooding, fire) -------------------------------------------------------- float holeCount = 0.0f; float waterAmount = 0.0f; - float totalHullVolume = 0.0f; + float dryHullVolume = 0.0f; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; } - if (hull.RoomName != null && hull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; } + if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (GameMain.GameSession?.GameMode is PvPMode) + { + if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; } + } + else + { + if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; } + } + fireAmount += hull.FireSources.Sum(fs => fs.Size.X); + if (hull.IsWetRoom) { continue; } foreach (Gap gap in hull.ConnectedGaps) { - if (!gap.IsRoomToRoom) holeCount += gap.Open; + if (!gap.IsRoomToRoom) + { + holeCount += gap.Open; + } } waterAmount += hull.WaterVolume; - totalHullVolume += hull.Volume; - fireAmount += hull.FireSources.Sum(fs => fs.Size.X); + dryHullVolume += hull.Volume; } - if (totalHullVolume > 0) + if (dryHullVolume > 0) { - floodingAmount = waterAmount / totalHullVolume; + floodingAmount = waterAmount / dryHullVolume; } //hull integrity at 0.0 if there are 10 or more wide-open holes @@ -779,7 +853,7 @@ namespace Barotrauma { if (level == null) { return 0.0f; } var refEntity = GetRefEntity(); - Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition); + Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition); var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) { @@ -897,15 +971,15 @@ namespace Barotrauma const int maxDist = 1000; - if (Level.Loaded != null) + if (level != null) { - foreach (var ruin in Level.Loaded.Ruins) + foreach (var ruin in level.Ruins) { Rectangle area = ruin.Area; area.Inflate(maxDist, maxDist); if (area.Contains(character.WorldPosition)) { return true; } } - foreach (var cave in Level.Loaded.Caves) + foreach (var cave in level.Caves) { Rectangle area = cave.Area; area.Inflate(maxDist, maxDist); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index d605651cc..b7632a54e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public readonly XElement ConfigElement; public readonly Type EventType; - public readonly float SpawnProbability; + public readonly float Probability; public readonly bool TriggerEventCooldown; public float Commonness; public string Identifier; @@ -39,7 +39,7 @@ namespace Barotrauma Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); - SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); + Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 16170a3b0..7f5ee22a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -48,10 +48,10 @@ namespace Barotrauma List eventPrefabs = new List(PrefabList); foreach (var eventSet in List) { - eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.First)); + eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.prefab)); foreach (var childSet in eventSet.ChildSets) { - eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.First)); + eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.prefab)); } } return eventPrefabs; @@ -96,8 +96,7 @@ namespace Barotrauma public readonly Dictionary Commonness; - //Pair.First: event prefab, Pair.Second: commonness - public readonly List> EventPrefabs; + public readonly List<(EventPrefab prefab, float commonness, float probability)> EventPrefabs; public readonly List ChildSets; @@ -111,7 +110,7 @@ namespace Barotrauma { DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier; Commonness = new Dictionary(); - EventPrefabs = new List>(); + EventPrefabs = new List<(EventPrefab prefab, float commonness, float probability)>(); ChildSets = new List(); BiomeIdentifier = element.GetAttributeString("biome", string.Empty); @@ -149,7 +148,7 @@ namespace Barotrauma OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); - Commonness[""] = 1.0f; + Commonness[""] = element.GetAttributeFloat("commonness", 1.0f); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -184,13 +183,14 @@ namespace Barotrauma else { float commonness = subElement.GetAttributeFloat("commonness", prefab.Commonness); - EventPrefabs.Add(new Pair( prefab, commonness)); + float probability = subElement.GetAttributeFloat("probability", prefab.Probability); + EventPrefabs.Add((prefab, commonness, probability)); } } else { var prefab = new EventPrefab(subElement); - EventPrefabs.Add(new Pair(prefab, prefab.Commonness)); + EventPrefabs.Add((prefab, prefab.Commonness, prefab.Probability)); } break; } @@ -342,13 +342,13 @@ namespace Barotrauma { if (thisSet.ChooseRandom) { - List> unusedEvents = new List>(thisSet.EventPrefabs); + var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(thisSet.EventPrefabs); for (int i = 0; i < thisSet.EventCount; i++) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Second).ToList(), Rand.RandSync.Unsynced); - if (eventPrefab != null) + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab.prefab != null) { - AddEvent(stats, eventPrefab.First); + AddEvent(stats, eventPrefab.prefab); unusedEvents.Remove(eventPrefab); } } @@ -357,7 +357,7 @@ namespace Barotrauma { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvent(stats, eventPrefab.First); + AddEvent(stats, eventPrefab.prefab); } } foreach (var childSet in thisSet.ChildSets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 0fa581532..55e7366cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -15,6 +16,10 @@ namespace Barotrauma protected readonly HashSet requireKill = new HashSet(); protected readonly HashSet requireRescue = new HashSet(); + private readonly string itemTag; + private readonly XElement itemConfig; + private readonly List items = new List(); + protected const int HostagesKilledState = 5; private readonly string hostagesKilledMessage; @@ -33,15 +38,55 @@ namespace Barotrauma } } + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + return Targets.Select(t => t.WorldPosition); + } + } + } + + private IEnumerable Targets + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else + { + if (items.Any()) + { + return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue); + } + else + { + return requireKill.Concat(requireRescue); + } + } + } + } + protected bool wasDocked; - public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) : - base(prefab, locations) + public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) : + base(prefab, locations, sub) { characterConfig = prefab.ConfigElement.Element("Characters"); string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag; + + itemConfig = prefab.ConfigElement.Element("Items"); + itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); } protected override void StartMissionSpecific(Level level) @@ -52,8 +97,13 @@ namespace Barotrauma characterItems.Clear(); requireKill.Clear(); requireRescue.Clear(); + items.Clear(); +#if SERVER + spawnedItems.Clear(); +#endif var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; + InitItems(submarine); if (!IsClient) { InitCharacters(submarine); @@ -62,56 +112,101 @@ namespace Barotrauma wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost); } + private void InitItems(Submarine submarine) + { + if (!string.IsNullOrEmpty(itemTag)) + { + var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); + if (!itemsToDestroy.Any()) + { + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + } + else + { + items.AddRange(itemsToDestroy); + } + } + + if (itemConfig != null && !IsClient) + { + foreach (XElement element in itemConfig.Elements()) + { + string itemIdentifier = element.GetAttributeString("identifier", ""); + if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); + continue; + } + + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( + SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, + moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); + if (spawnPoint == null) + { + spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + Vector2 spawnPos = spawnPoint.WorldPosition; + if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) + { + spawnPos = new Vector2( + MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50), + wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f); + } + var item = new Item(itemPrefab, spawnPos, null); + items.Add(item); +#if SERVER + spawnedItems.Add(item); +#endif + } + } + } + private void InitCharacters(Submarine submarine) { characters.Clear(); characterItems.Clear(); - if (characterConfig == null) { return; } - - foreach (XElement element in characterConfig.Elements()) - { - if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; } - - int defaultCount = element.GetAttributeInt("count", -1); - if (defaultCount < 0) + if (characterConfig != null) + { + foreach (XElement element in characterConfig.Elements()) { - defaultCount = element.GetAttributeInt("amount", 1); - } - int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255); - int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255); - int count = Rand.Range(min, max + 1); + if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; } - if (element.Attribute("identifier") != null && element.Attribute("from") != null) - { - string characterIdentifier = element.GetAttributeString("identifier", ""); - string characterFrom = element.GetAttributeString("from", ""); - HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); - if (humanPrefab == null) + int defaultCount = element.GetAttributeInt("count", -1); + if (defaultCount < 0) { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found"); - continue; + defaultCount = element.GetAttributeInt("amount", 1); } - for (int i = 0; i < count; i++) + int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255); + int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255); + int count = Rand.Range(min, max + 1); + + if (element.Attribute("identifier") != null && element.Attribute("from") != null) { - LoadHuman(humanPrefab, element, submarine); + HumanPrefab humanPrefab = GetHumanPrefabFromElement(element); + for (int i = 0; i < count; i++) + { + LoadHuman(humanPrefab, element, submarine); + } + } + else + { + string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", "")); + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab == null) + { + DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); + continue; + } + for (int i = 0; i < count; i++) + { + LoadMonster(characterPrefab, element, submarine); + } } } - else - { - string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", "")); - var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); - if (characterPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found"); - continue; - } - for (int i = 0; i < count; i++) - { - LoadMonster(characterPrefab, element, submarine); - } - } - } + } } private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine) @@ -128,32 +223,27 @@ namespace Barotrauma spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); - Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); - if (element.GetAttributeBool("requirerescue", false)) - { - requireRescue.Add(spawnedCharacter); - spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC; -#if CLIENT - GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); -#endif - } - else - { - spawnedCharacter.TeamID = CharacterTeamType.None; - } - humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos); - humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false); + bool requiresRescue = element.GetAttributeBool("requirerescue", false); + + Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true); + if (spawnPos is WayPoint wp) { spawnedCharacter.GiveIdCardTags(wp); } + + if (requiresRescue) + { + requireRescue.Add(spawnedCharacter); +#if CLIENT + GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter); +#endif + } + if (element.GetAttributeBool("requirekill", false)) { requireKill.Add(spawnedCharacter); } - characters.Add(spawnedCharacter); - characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); } private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine) @@ -187,7 +277,7 @@ namespace Barotrauma } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (State != HostagesKilledState) { @@ -215,7 +305,8 @@ namespace Barotrauma { case 0: - if (requireKill.All(c => c.Removed || c.IsDead) && + if (items.All(it => it.Removed || it.Condition <= 0.0f) && + requireKill.All(c => c.Removed || c.IsDead) && requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) { State = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index b3a5365f1..463da3632 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -13,7 +13,7 @@ namespace Barotrauma private Point monsterCountRange; private readonly string sonarLabel; - public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) + public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { swarmSpawned = false; @@ -53,7 +53,7 @@ namespace Barotrauma } } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { return; } if (!swarmSpawned && level.CheckBeaconActive()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index f5215e725..8364eedca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -15,17 +15,112 @@ namespace Barotrauma private readonly Dictionary parentInventoryIDs = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); - private int requiredDeliveryAmount; + private float requiredDeliveryAmount; - public CargoMission(MissionPrefab prefab, Location[] locations) - : base(prefab, locations) + private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>(); + private int? rewardPerCrate; + private int calculatedReward; + private int maxItemCount; + + private Submarine sub; + + public override string Description { + get + { + if (Submarine.MainSub != sub) + { + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + } + return description; + } + } + + public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + this.sub = sub; itemConfig = prefab.ConfigElement.Element("Items"); - requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0); + requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f); + DetermineCargo(); + } + + private void DetermineCargo() + { + if (this.sub == null || itemConfig == null) + { + calculatedReward = Prefab.Reward; + return; + } + + itemsToSpawn.Clear(); + List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers(); + containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); }); + + maxItemCount = 0; + foreach (XElement subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + maxItemCount += maxCount; + } + + for (int i = 0; i < containers.Count; i++) + { + foreach (XElement subElement in itemConfig.Elements()) + { + int maxCount = subElement.GetAttributeInt("maxcount", 10); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } + ItemPrefab itemPrefab = FindItemPrefab(subElement); + while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab)) + { + containers[i] = (containers[i].container, containers[i].freeSlots - 1); + itemsToSpawn.Add((subElement, containers[i].container)); + if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; } + } + } + } + + if (!itemsToSpawn.Any()) + { + itemsToSpawn.Add((itemConfig.Elements().First(), null)); + } + + calculatedReward = 0; + foreach (var itemToSpawn in itemsToSpawn) + { + int price = itemToSpawn.element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count); + if (rewardPerCrate.HasValue) + { + if (price != rewardPerCrate.Value) { rewardPerCrate = -1; } + } + else + { + rewardPerCrate = price; + } + calculatedReward += price; + } + if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; } + + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + } + + public override int GetReward(Submarine sub) + { + if (sub != this.sub) + { + this.sub = sub; + DetermineCargo(); + } + return calculatedReward; } private void InitItems() { + this.sub = Submarine.MainSub; + DetermineCargo(); + items.Clear(); parentInventoryIDs.Clear(); parentItemContainerIndices.Clear(); @@ -36,20 +131,15 @@ namespace Barotrauma return; } - foreach (XElement subElement in itemConfig.Elements()) + foreach (var (element, container) in itemsToSpawn) { - LoadItemAsChild(subElement, null); + LoadItemAsChild(element, container?.Item); } - if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; } - if (requiredDeliveryAmount > items.Count) - { - DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver."); - requiredDeliveryAmount = items.Count; - } + if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } - private void LoadItemAsChild(XElement element, Item parent) + private ItemPrefab FindItemPrefab(XElement element) { ItemPrefab itemPrefab; if (element.Attribute("name") != null) @@ -60,7 +150,6 @@ namespace Barotrauma if (itemPrefab == null) { DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found"); - return; } } else @@ -70,15 +159,15 @@ namespace Barotrauma if (itemPrefab == null) { DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found"); - return; } } + return itemPrefab; + } - if (itemPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found"); - return; - } + + private void LoadItemAsChild(XElement element, Item parent) + { + ItemPrefab itemPrefab = FindItemPrefab(element); WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); if (cargoSpawnPos == null) @@ -88,7 +177,6 @@ namespace Barotrauma } var cargoRoom = cargoSpawnPos.CurrentHull; - if (cargoRoom == null) { DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); @@ -140,7 +228,7 @@ namespace Barotrauma if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); - if (deliveredItemCount >= requiredDeliveryAmount) + if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { GiveReward(); completed = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 054436bfd..c1f7f26be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Barotrauma.Items.Components; +using Barotrauma.Extensions; using System.Collections.Generic; namespace Barotrauma @@ -7,6 +6,7 @@ namespace Barotrauma partial class CombatMission : Mission { private Submarine[] subs; + // TODO: not used private List[] crews; private readonly string[] descriptions; @@ -45,8 +45,8 @@ namespace Barotrauma } } - public CombatMission(MissionPrefab prefab, Location[] locations) - : base(prefab, locations) + public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) { descriptions = new string[] { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs new file mode 100644 index 000000000..bffec355a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -0,0 +1,344 @@ +using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class EscortMission : Mission + { + private readonly XElement characterConfig; + private readonly XElement itemConfig; + + private readonly List characters = new List(); + private readonly Dictionary> characterItems = new Dictionary>(); + + private readonly int baseEscortedCharacters; + private readonly float scalingEscortedCharacters; + private readonly float terroristChance; + + private int calculatedReward; + private Submarine missionSub; + + private Character vipCharacter; + + private readonly List terroristCharacters = new List(); + private bool terroristsShouldAct = false; + private float terroristDistanceSquared; + private const string TerroristTeamChangeIdentifier = "terrorist"; + + public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + missionSub = sub; + characterConfig = prefab.ConfigElement.Element("Characters"); + baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1); + scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); + terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); + itemConfig = prefab.ConfigElement.Element("TerroristItems"); + CalculateReward(); + } + + private void CalculateReward() + { + if (missionSub == null) + { + calculatedReward = Prefab.Reward; + return; + } + + int multiplier = CalculateScalingEscortedCharacterCount(); + calculatedReward = Prefab.Reward * multiplier; + + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + } + + public override int GetReward(Submarine sub) + { + if (sub != missionSub) + { + missionSub = sub; + CalculateReward(); + } + return calculatedReward; + } + + int CalculateScalingEscortedCharacterCount(bool inMission = false) + { + if (missionSub == null || missionSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed + { + if (inMission) + { + DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!"); + } + return 1; + } + return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (missionSub.Info.RecommendedCrewSizeMin + missionSub.Info.RecommendedCrewSizeMax) / 2); + } + + private void InitEscort() + { + characters.Clear(); + characterItems.Clear(); + + WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); + Rand.RandSync randSync = Rand.RandSync.Server; + + if (terroristChance > 0f) + { + // in terrorist missions, reroll characters each retry to avoid confusion as to who the terrorists are + randSync = Rand.RandSync.Unsynced; + } + + //if any of the escortees have a job defined, try to use a spawnpoint designated for that job + foreach (XElement element in characterConfig.Elements()) + { + var humanPrefab = GetHumanPrefabFromElement(element); + if (humanPrefab == null || string.IsNullOrEmpty(humanPrefab.Job) || humanPrefab.Job.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + + var jobPrefab = humanPrefab.GetJobPrefab(); + if (jobPrefab != null) + { + var jobSpecificSpawnPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, Submarine.MainSub); + if (jobSpecificSpawnPos != null) + { + explicitStayInHullPos = jobSpecificSpawnPos; + break; + } + } + } + + foreach (XElement element in characterConfig.Elements()) + { + int count = CalculateScalingEscortedCharacterCount(inMission: true); + for (int i = 0; i < count; i++) + { + Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(element), characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); + if (spawnedCharacter.AIController is HumanAIController humanAI) + { + humanAI.InitMentalStateManager(); + } + } + } + + if (terroristChance > 0f) + { + int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count); + terroristCount = Math.Clamp(terroristCount, 1, characters.Count); + + terroristCharacters.Clear(); + characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c)); + + terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f); + +#if DEBUG + DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared)); + foreach (Character character in terroristCharacters) + { + DebugConsole.AddWarning(character.Name + " is a terrorist."); + } +#endif + } + } + + private void InitCharacters() + { + int scalingCharacterCount = CalculateScalingEscortedCharacterCount(inMission: true); + + if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count) + { + DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission"); + return; + } + int i = 0; + + foreach (XElement element in characterConfig.Elements()) + { + string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty); + string colorIdentifier = element.GetAttributeString("color", string.Empty); + for (int k = 0; k < scalingCharacterCount; k++) + { + // for each element defined, we need to initialize that type of character equal to the scaling escorted character count + characters[k + i].IsEscorted = true; + if (escortIdentifier != string.Empty) + { + if (escortIdentifier == "vip") + { + vipCharacter = characters[k + i]; + } + } + characters[k + i].UniqueNameColor = element.GetAttributeColor("color", Color.LightGreen); + } + i++; + } + } + + protected override void StartMissionSpecific(Level level) + { + if (characters.Count > 0) + { +#if DEBUG + throw new Exception($"characters.Count > 0 ({characters.Count})"); +#else + DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds."); + characters.Clear(); +#endif + } + + if (characterConfig == null) + { + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + return; + } + + // to ensure single missions run without issues, default to mainsub + if (missionSub == null) + { + missionSub = Submarine.MainSub; + CalculateReward(); + } + + if (!IsClient) + { + InitEscort(); + InitCharacters(); + } + } + + void TryToTriggerTerrorists() + { + if (terroristsShouldAct) + { + // decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards + foreach (Character character in terroristCharacters) + { + if (character.HasTeamChange(TerroristTeamChangeIdentifier)) + { + // already triggered + continue; + } + + if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) + { + character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); + character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f)); + XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); + if (randomElement != null) + { + HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true); + } + } + } + } + else if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) < terroristDistanceSquared) + { + foreach (Character character in terroristCharacters) + { + if (character.AIController is HumanAIController humanAI) + { + humanAI.ObjectiveManager.AddObjective(new AIObjectiveEscapeHandcuffs(character, humanAI.ObjectiveManager, shouldSwitchTeams: false, beginInstantly: true)); + } + } + terroristsShouldAct = true; + } + } + + bool NonTerroristsStillAlive(IEnumerable characterList) + { + return characterList.All(c => terroristCharacters.Contains(c) || IsAlive(c)); + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + if (!IsClient) + { + int newState = State; + TryToTriggerTerrorists(); + switch (State) + { + case 0: // base + if (!NonTerroristsStillAlive(characters)) + { + newState = 1; + } + if (terroristCharacters.Any() && terroristCharacters.All(c => !IsAlive(c))) + { + newState = 2; + } + break; + case 1: // failure + break; + case 2: // terrorists killed + if (!NonTerroristsStillAlive(characters)) + { + newState = 1; + } + break; + } + State = newState; + } + } + + private bool Survived(Character character) + { + return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub; + } + + private bool IsAlive(Character character) + { + return character != null && !character.Removed && !character.IsDead; + } + + private bool IsCaptured(Character character) + { + return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier); + } + + public override void End() + { + if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) + { + bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c)); + bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c)); + bool vipDied = false; + + // this logic is currently irrelevant, as the mission is failed regardless of who dies + if (vipCharacter != null) + { + vipDied = !Survived(vipCharacter); + } + + if (friendliesSurvived && !terroristsSurvived && !vipDied) + { + GiveReward(); + completed = true; + } + } + + // characters that survived will take their items with them, in case players tried to be crafty and steal them + // this needs to run here in case players abort the mission by going back home + // TODO: I think this might feel like a bug. + foreach (var characterItem in characterItems) + { + if (Survived(characterItem.Key) || !completed) + { + foreach (Item item in characterItem.Value) + { + if (!item.Removed) + { + item.Remove(); + } + } + } + } + + characters.Clear(); + characterItems.Clear(); + failed = !completed; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs new file mode 100644 index 000000000..3b077be2c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/GoToMission.cs @@ -0,0 +1,28 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class GoToMission : Mission + { + public GoToMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) + { + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + State = 1; + } + +#if CLIENT + public override void ClientReadInitial(IReadMessage msg) + { + } +#elif SERVER + + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + } +#endif + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index ef15b10df..04278f4d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -26,7 +26,7 @@ namespace Barotrauma } } - public MineralMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) + public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { var configElement = prefab.ConfigElement.Element("Items"); foreach (var c in configElement.GetChildElements("Item")) @@ -115,7 +115,7 @@ namespace Barotrauma FindRelevantLevelResources(); } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { return; } switch (State) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index fa3d252da..516949ec1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -22,6 +23,7 @@ namespace Barotrauma if (state != value) { state = value; + TryTriggerEvents(state); #if SERVER GameMain.Server?.UpdateMissionState(this, state); #endif @@ -61,14 +63,19 @@ namespace Barotrauma //private set { description = value; } } + protected string descriptionWithoutReward; + public virtual bool AllowUndocking { get { return true; } } - public int Reward + public virtual int Reward { - get { return Prefab.Reward; } + get + { + return Prefab.Reward; + } } public Dictionary ReputationRewards @@ -92,6 +99,16 @@ namespace Barotrauma get { return true; } } + public virtual int TeamCount + { + get { return 1; } + } + + public virtual SubmarineInfo EnemySubmarineInfo + { + get { return null; } + } + public virtual IEnumerable SonarPositions { get { return Enumerable.Empty(); } @@ -112,8 +129,22 @@ namespace Barotrauma { get { return Prefab.Difficulty; } } + + private class DelayedTriggerEvent + { + public readonly MissionPrefab.TriggerEvent TriggerEvent; + public float Delay; + + public DelayedTriggerEvent(MissionPrefab.TriggerEvent triggerEvent, float delay) + { + TriggerEvent = triggerEvent; + Delay = delay; + } + } + + private List delayedTriggerEvents = new List(); - public Mission(MissionPrefab prefab, Location[] locations) + public Mission(MissionPrefab prefab, Location[] locations, Submarine sub) { System.Diagnostics.Debug.Assert(locations.Length == 2); @@ -138,8 +169,12 @@ namespace Barotrauma Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName); } } - string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖"; - if (description != null) { description = description.Replace("[reward]", rewardText); } + string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; + if (description != null) + { + descriptionWithoutReward = description; + description = description.Replace("[reward]", rewardText); + } if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } for (int m = 0; m < Messages.Count; m++) @@ -147,6 +182,9 @@ namespace Barotrauma Messages[m] = Messages[m].Replace("[reward]", rewardText); } } + + public virtual void SetDifficulty(float difficulty) { } + public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) { return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer); @@ -181,7 +219,7 @@ namespace Barotrauma { if (randomNumber <= missionPrefab.Commonness) { - return missionPrefab.Instantiate(locations); + return missionPrefab.Instantiate(locations, Submarine.MainSub); } randomNumber -= missionPrefab.Commonness; } @@ -189,11 +227,18 @@ namespace Barotrauma return null; } + public virtual int GetReward(Submarine sub) + { + return Prefab.Reward; + } + public void Start(Level level) { + state = 0; #if CLIENT shownMessages.Clear(); #endif + delayedTriggerEvents.Clear(); foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) { foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false)) @@ -202,12 +247,27 @@ namespace Barotrauma } } this.level = level; + TryTriggerEvents(0); StartMissionSpecific(level); } protected virtual void StartMissionSpecific(Level level) { } - public virtual void Update(float deltaTime) { } + public void Update(float deltaTime) + { + for (int i = delayedTriggerEvents.Count - 1; i>=0;i--) + { + delayedTriggerEvents[i].Delay -= deltaTime; + if (delayedTriggerEvents[i].Delay <= 0.0f) + { + TriggerEvent(delayedTriggerEvents[i].TriggerEvent); + delayedTriggerEvents.RemoveAt(i); + } + } + UpdateMissionSpecific(deltaTime); + } + + protected virtual void UpdateMissionSpecific(float deltaTime) { } protected void ShowMessage(int missionState) { @@ -216,6 +276,57 @@ namespace Barotrauma partial void ShowMessageProjSpecific(int missionState); + + private void TryTriggerEvents(int state) + { + foreach (var triggerEvent in Prefab.TriggerEvents) + { + if (triggerEvent.State == state) + { + TryTriggerEvent(triggerEvent); + } + } + } + + /// + /// Triggers the event or adds it to the delayedTriggerEvents it if it has a delay + /// + private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger) + { + if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } + if (trigger.Delay > 0) + { + if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger)) + { + delayedTriggerEvents.Add(new DelayedTriggerEvent(trigger, trigger.Delay)); + } + } + else + { + TriggerEvent(trigger); + } + } + + /// + /// Triggers the event immediately, ignoring any delays + /// + private void TriggerEvent(MissionPrefab.TriggerEvent trigger) + { + if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } + var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase)); + if (eventPrefab == null) + { + DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\")."); + return; + } + if (GameMain.GameSession?.EventManager != null) + { + var newEvent = eventPrefab.CreateInstance(); + GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent); + newEvent.Init(true); + } + } + /// /// End the mission and give a reward if it was completed successfully /// @@ -232,7 +343,7 @@ namespace Barotrauma public void GiveReward() { if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; } - campaign.Money += Reward; + campaign.Money += GetReward(Submarine.MainSub); foreach (KeyValuePair reputationReward in ReputationRewards) { @@ -287,5 +398,47 @@ namespace Barotrauma } public virtual void AdjustLevelData(LevelData levelData) { } + + // putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition) + protected HumanPrefab GetHumanPrefabFromElement(XElement element) + { + if (element.Attribute("name") != null) + { + DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); + + return null; + } + + string characterIdentifier = element.GetAttributeString("identifier", ""); + string characterFrom = element.GetAttributeString("from", ""); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found"); + return null; + } + + return humanPrefab; + } + + protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true) + { + if (positionToStayIn == null) + { + positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine); + } + + var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + characterInfo.TeamID = teamType; + Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); + spawnedCharacter.Prefab = humanPrefab; + humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); + humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false); + + characters.Add(spawnedCharacter); + characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); + + return spawnedCharacter; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 274f041cf..80be751bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -18,10 +18,11 @@ namespace Barotrauma Nest = 0x10, Mineral = 0x20, Combat = 0x40, - OutpostDestroy = 0x80, - OutpostRescue = 0x100, - - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue + AbandonedOutpost = 0x80, + Escort = 0x100, + Pirate = 0x200, + GoTo = 0x400, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo } partial class MissionPrefab @@ -36,13 +37,17 @@ namespace Barotrauma { MissionType.Beacon, typeof(BeaconMission) }, { MissionType.Nest, typeof(NestMission) }, { MissionType.Mineral, typeof(MineralMission) }, - { MissionType.OutpostDestroy, typeof(OutpostDestroyMission) }, - { MissionType.OutpostRescue, typeof(AbandonedOutpostMission) }, + { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) }, + { MissionType.Escort, typeof(EscortMission) }, + { MissionType.Pirate, typeof(PirateMission) }, + { MissionType.GoTo, typeof(GoToMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { { MissionType.Combat, typeof(CombatMission) } }; + + public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; private readonly ConstructorInfo constructor; @@ -84,6 +89,8 @@ namespace Barotrauma public readonly bool IsSideObjective; + public readonly bool RequireWreck; + /// /// The mission can only be received when travelling from Pair.First to Pair.Second /// @@ -99,6 +106,28 @@ namespace Barotrauma /// public readonly List UnhideEntitySubCategories = new List(); + public class TriggerEvent + { + [Serialize("", true)] + public string EventIdentifier { get; private set; } + + [Serialize(0, true)] + public int State { get; private set; } + + [Serialize(0.0f, true)] + public float Delay { get; private set; } + + [Serialize(false, true)] + public bool CampaignOnly { get; private set; } + + public TriggerEvent(XElement element) + { + SerializableProperty.DeserializeProperties(this, element); + } + } + + public readonly List TriggerEvents = new List(); + public LocationTypeChange LocationTypeChangeOnCompleted; public readonly XElement ConfigElement; @@ -157,6 +186,7 @@ namespace Barotrauma Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); + RequireWreck = element.GetAttributeBool("requirewreck", false); Commonness = element.GetAttributeInt("commonness", 1); if (element.GetAttribute("difficulty") != null) { @@ -269,10 +299,19 @@ namespace Barotrauma DataRewards.Add(Tuple.Create(identifier, value, operation)); } break; + case "triggerevent": + TriggerEvents.Add(new TriggerEvent(subElement)); + break; } } string missionTypeName = element.GetAttributeString("type", ""); + //backwards compatibility + if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase)) + { + missionTypeName = "AbandonedOutpost"; + } + if (!Enum.TryParse(missionTypeName, out Type)) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); @@ -286,16 +325,20 @@ namespace Barotrauma if (CoOpMissionClasses.ContainsKey(Type)) { - constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }); } else if (PvPMissionClasses.ContainsKey(Type)) { - constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }); } else { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); } + if (constructor == null) + { + DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!"); + } InitProjSpecific(element); } @@ -333,9 +376,9 @@ namespace Barotrauma return false; } - public Mission Instantiate(Location[] locations) + public Mission Instantiate(Location[] locations, Submarine sub) { - return constructor?.Invoke(new object[] { this, locations }) as Mission; + return constructor?.Invoke(new object[] { this, locations, sub }) as Mission; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index bf83c9e61..31109da8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -33,8 +33,8 @@ namespace Barotrauma } } - public MonsterMission(MissionPrefab prefab, Location[] locations) - : base(prefab, locations) + public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) { string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null); if (!string.IsNullOrEmpty(speciesName)) @@ -160,7 +160,7 @@ namespace Barotrauma } } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { switch (State) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index b97ade2ee..0dc3ce997 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -46,8 +46,8 @@ namespace Barotrauma } } - public NestMission(MissionPrefab prefab, Location[] locations) - : base(prefab, locations) + public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.Element("Items"); @@ -216,7 +216,7 @@ namespace Barotrauma level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount); } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs deleted file mode 100644 index 029db236f..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma -{ - partial class OutpostDestroyMission : AbandonedOutpostMission - { - private readonly string itemTag; - private readonly XElement itemConfig; - private readonly List items = new List(); - - public override IEnumerable SonarPositions - { - get - { - if (State > 0) - { - return Enumerable.Empty(); - } - else - { - return Targets.Select(t => t.WorldPosition); - } - } - } - - private IEnumerable Targets - { - get - { - if (State > 0) - { - return Enumerable.Empty(); - } - else - { - if (items.Any()) - { - return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue); - } - else - { - return requireKill.Concat(requireRescue); - } - } - } - } - - public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) : - base(prefab, locations) - { - itemConfig = prefab.ConfigElement.Element("Items"); - itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); - } - - protected override void StartMissionSpecific(Level level) - { - items.Clear(); -#if SERVER - spawnedItems.Clear(); -#endif - if (!string.IsNullOrEmpty(itemTag)) - { - var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); - if (!itemsToDestroy.Any()) - { - DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); - } - else - { - items.AddRange(itemsToDestroy); - } - } - if (itemConfig != null && !IsClient) - { - foreach (XElement element in itemConfig.Elements()) - { - string itemIdentifier = element.GetAttributeString("identifier", ""); - if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) - { - DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found"); - continue; - } - - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); - ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( - SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, - moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); - if (spawnPoint == null) - { - var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); - } - Vector2 spawnPos = spawnPoint.WorldPosition; - if (spawnPoint is WayPoint wp && wp.CurrentHull != null) - { - spawnPos = new Vector2( - MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right), - wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f); - } - var item = new Item(itemPrefab, spawnPos, null); - items.Add(item); -#if SERVER - spawnedItems.Add(item); -#endif - } - } - - base.StartMissionSpecific(level); - } - - public override void Update(float deltaTime) - { - if (requireRescue.Any(r => r.Removed || r.IsDead)) - { -#if SERVER - if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) - { - GameMain.Server.EndGame(); - } -#endif - return; - } - - switch (state) - { - case 0: - if (items.Any()) - { - if (items.All(it => it.Removed || it.Condition <= 0.0f) && - requireKill.All(c => c.Removed || c.IsDead) && - requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) - { - State = 1; - } - } - else - { - if (requireKill.All(c => c.Removed || c.IsDead) && - requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player)) - { - State = 1; - } - } - break; -#if SERVER - case 1: - if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null) - { - if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost))) - { - GameMain.Server.EndGame(); - State = 2; - } - } - break; -#endif - } - } - } -} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..57b63b0b7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -0,0 +1,395 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class PirateMission : Mission + { + private readonly XElement submarineTypeConfig; + private readonly XElement characterConfig; + private readonly XElement characterTypeConfig; + private readonly float addedMissionDifficultyPerPlayer; + + private float missionDifficulty; + private int alternateReward; + + private Submarine enemySub; + private readonly List characters = new List(); + private readonly Dictionary> characterItems = new Dictionary>(); + + // Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it. + private readonly float pirateSightingUpdateFrequency = 30; + private float pirateSightingUpdateTimer; + private Vector2? lastSighting; + + public override int TeamCount => 2; + + private bool outsideOfSonarRange; + + private readonly List patrolPositions = new List(); + + public override IEnumerable SonarPositions + { + get + { + var empty = Enumerable.Empty(); + if (outsideOfSonarRange) + { + return State switch + { + 0 => patrolPositions, + 1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty, + _ => empty, + }; + } + else + { + return empty; + } + } + } + + public override int GetReward(Submarine sub) + { + return alternateReward; + } + + private SubmarineInfo submarineInfo; + + public override SubmarineInfo EnemySubmarineInfo + { + get + { + return submarineInfo; + } + } + + // these values could also be defined within the mission XML + private const float RandomnessModifier = 25; + private const float ShipRandomnessModifier = 15; + + private const float MaxDifficulty = 100; + + public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes"); + characterConfig = prefab.ConfigElement.Element("Characters"); + characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes"); + addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); + + // for campaign missions, set difficulty at construction + LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData; + + SetDifficulty(levelData?.Difficulty ?? Level.Loaded?.Difficulty ?? 0f); + } + + public override void SetDifficulty(float difficulty) + { + if (missionDifficulty > 0f) + { + // difficulty already set + return; + } + + missionDifficulty = difficulty; + + XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier); + + alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); + + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + + string submarinePath = submarineConfig.GetAttributeString("path", string.Empty); + if (submarinePath == string.Empty) + { + DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); + return; + } + // maybe a little redundant + var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath); + if (contentFile == null) + { + DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); + return; + } + + submarineInfo = new SubmarineInfo(contentFile.Path); + } + + private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier) + { + return Math.Abs(levelDifficulty - preferredDifficulty + (Rand.Range(-randomnessModifier, randomnessModifier, Rand.RandSync.Server))); + } + private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty) + { + return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * ((levelDifficulty + Rand.Range(-RandomnessModifier, RandomnessModifier, Rand.RandSync.Server)) / MaxDifficulty)), minAmount); + } + + private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier) + { + // look for the element that is closest to our difficulty, with some randomness + XElement bestElement = null; + float bestValue = float.MaxValue; + foreach (XElement element in parentElement.Elements()) + { + float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier); + if (applicabilityValue < bestValue) + { + bestElement = element; + bestValue = applicabilityValue; + } + } + return bestElement; + } + + private void CreateMissionPositions(out Vector2 preferredSpawnPos) + { + Vector2 patrolPos = enemySub.WorldPosition; + Point subSize = enemySub.GetDockedBorders().Size; + + if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos)) + { + DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this); + } + if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f)) + { + DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this); + } + + patrolPos = enemySub.FindSpawnPos(patrolPos, subSize); + + patrolPositions.Add(patrolPos); + patrolPositions.Add(preferredSpawnPos); + + if (!IsClient) + { + PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false); + var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos)); + if (!path.Unreachable) + { + preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible + } + + int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead + preferredSpawnPos = enemySub.FindSpawnPos(preferredSpawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance)); + } + } + + private void InitPirateShip(Vector2 spawnPos) + { + enemySub.NeutralizeBallast(); + if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + { + reactor.PowerUpImmediately(); + } + enemySub.EnableMaintainPosition(); + enemySub.TeamID = CharacterTeamType.None; + //make the enemy sub withstand atleast the same depth as the player sub + enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth); + } + + private void InitPirates() + { + characters.Clear(); + characterItems.Clear(); + + if (characterConfig == null) + { + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + return; + } + + int playerCount = 1; + +#if SERVER + playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count(); +#endif + + float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer; + + bool commanderAssigned = false; + foreach (XElement element in characterConfig.Elements()) + { + // 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); + for (int i = 0; i < amountCreated; i++) + { + XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault(); + + if (characterType == null) + { + DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\"."); + return; + } + + XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); + + Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null); + if (!commanderAssigned) + { + bool isCommander = variantElement.GetAttributeBool("iscommander", false); + if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController) + { + humanAIController.InitShipCommandManager(); + foreach (var patrolPos in patrolPositions) + { + humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos); + } + commanderAssigned = true; + } + } + + foreach (Item item in spawnedCharacter.Inventory.AllItems) + { + if (item?.Prefab.Identifier == "idcard") + { + item.AddTag("id_pirate"); + } + } + } + } + } + + protected override void StartMissionSpecific(Level level) + { + if (characters.Count > 0) + { +#if DEBUG + throw new Exception($"characters.Count > 0 ({characters.Count})"); +#else + DebugConsole.AddWarning("Character list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds."); + characters.Clear(); +#endif + } + + if (patrolPositions.Count > 0) + { +#if DEBUG + throw new Exception($"patrolPositions.Count > 0 ({patrolPositions.Count})"); +#else + DebugConsole.AddWarning("Patrol point list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds."); + patrolPositions.Clear(); +#endif + } + + enemySub = Submarine.MainSubs[1]; + + if (enemySub == null) + { + DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined."); + // TODO: should we set the state to something here? + return; + } + + Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen + CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them +#if DEBUG + if (IsClient) + { + DebugConsole.NewMessage("The patrol positions set by client were: "); + } + else + { + DebugConsole.NewMessage("The patrol positions set by server were: "); + } + foreach (var patrolPos in patrolPositions) + { + DebugConsole.NewMessage("Patrol pos: " + patrolPos); + } +#endif + if (!IsClient) + { + InitPirateShip(spawnPos); + } + enemySub.SetPosition(spawnPos); + + // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine + // creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing + enemySub.FlipX(); + enemySub.ShowSonarMarker = false; + + if (!IsClient) + { + InitPirates(); + } + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + int newState = State; + float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange); + outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange; + if (State < 2 && CheckWinState()) + { + newState = 2; + } + else + { + switch (State) + { + case 0: + for (int i = patrolPositions.Count - 1; i >= 0; i--) + { + if (Vector2.DistanceSquared(patrolPositions[i], Submarine.MainSub.WorldPosition) < sqrSonarRange) + { + patrolPositions.RemoveAt(i); + } + } + if (!outsideOfSonarRange || patrolPositions.None()) + { + newState = 1; + } + break; + case 1: + if (outsideOfSonarRange) + { + if (lastSighting.HasValue && Vector2.DistanceSquared(lastSighting.Value, Submarine.MainSub.WorldPosition) < sqrSonarRange) + { + lastSighting = null; + } + pirateSightingUpdateTimer -= deltaTime; + if (pirateSightingUpdateTimer < 0) + { + pirateSightingUpdateTimer = pirateSightingUpdateFrequency; + lastSighting = enemySub.WorldPosition; + } + } + else + { + lastSighting = enemySub.WorldPosition; + pirateSightingUpdateTimer = 0; + } + break; + } + } + State = newState; + } + + private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m))); + + private bool Survived(Character character) + { + return character != null && !character.Removed && !character.IsDead; + } + + public override void End() + { + if (state == 2) + { + GiveReward(); + completed = true; + } + characters.Clear(); + characterItems.Clear(); + failed = !completed; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 4738a3a66..36f611211 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -43,8 +43,8 @@ namespace Barotrauma } } - public SalvageMission(MissionPrefab prefab, Location[] locations) - : base(prefab, locations) + public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub) + : base(prefab, locations, sub) { containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); @@ -205,7 +205,7 @@ namespace Barotrauma } } - public override void Update(float deltaTime) + protected override void UpdateMissionSpecific(float deltaTime) { if (item == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e7bf21f0e..3edb99bdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -88,7 +88,7 @@ namespace Barotrauma } offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); - scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000); + scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); if (GameMain.NetworkMember != null) { @@ -182,18 +182,11 @@ namespace Barotrauma { if (disallowed) { return; } - if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) - { - spawnPos = null; - Finished(); - return; - } - spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); - bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck; - if (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss) + bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck); + if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss)) { if (availablePositions.None()) { @@ -264,7 +257,7 @@ namespace Barotrauma } else { - if (!isSubOrWreck) + if (!isRuinOrWreck) { float minDistance = 20000; var refSub = GetReferenceSub(); @@ -375,7 +368,7 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss) + if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss)) { foreach (Submarine submarine in Submarine.Loaded) { @@ -387,7 +380,7 @@ namespace Barotrauma //if spawning in a ruin/cave, wait for someone to be close to it to spawning //unnecessary monsters in places the players might never visit during the round - if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck) + if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck)) { bool someoneNearby = false; float minDist = Sonar.DefaultSonarRange * 0.8f; @@ -415,16 +408,19 @@ namespace Barotrauma } - if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave) + if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave)) { + bool anyInAbyss = false; foreach (Submarine submarine in Submarine.Loaded) { - if (submarine.Info.Type != SubmarineType.Player) { continue; } - if (submarine.WorldPosition.Y > 0) + if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; } + if (submarine.WorldPosition.Y < 0) { - return; + anyInAbyss = true; + break; } } + if (!anyInAbyss) { return; } } spawnPending = false; @@ -432,7 +428,23 @@ namespace Barotrauma //+1 because Range returns an integer less than the max value int amount = Rand.Range(minAmount, maxAmount + 1); monsters = new List(); - float offsetAmount = spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath ? scatter : 100; + float scatterAmount = scatter; + if (spawnPosType.HasFlag(Level.PositionType.SidePath)) + { + var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath); + if (sidePaths.Any()) + { + scatterAmount = Math.Min(scatter, sidePaths.Min(t => t.MinWidth) / 2); + } + else + { + scatterAmount = scatter; + } + } + else if (!spawnPosType.HasFlag(Level.PositionType.MainPath)) + { + scatterAmount = 0; + } for (int i = 0; i < amount; i++) { string seed = Level.Loaded.Seed + i.ToString(); @@ -443,8 +455,8 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); - Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount); - if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath) + Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount); + if (scatterAmount > 0) { if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 040f24a6a..c3d164708 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -86,10 +86,11 @@ namespace Barotrauma.Extensions /// /// Executes an action that modifies the collection on each element (such as removing items from the list). - /// Creates a temporary list. + /// Creates a temporary list, unless the collection is empty. /// public static void ForEachMod(this IEnumerable source, Action action) { + if (source.None()) { return; } var temp = new List(source); temp.ForEach(action); } @@ -153,5 +154,22 @@ namespace Barotrauma.Extensions { if (value != null) { source.Add(value); } } + + /// + /// Returns whether a given collection has at least a certain amount + /// of elements for which the predicate returns true. + /// + /// Input collection + /// How many elements to match before stopping + /// Predicate used to evaluate the elements + public static bool AtLeast(this IEnumerable source, int amount, Predicate predicate) + { + foreach (T elem in source) + { + if (predicate(elem)) { amount--; } + if (amount <= 0) { return true; } + } + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index f2e62e40d..c93b28077 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -207,7 +207,8 @@ namespace Barotrauma SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, - OriginalContainerID = validContainer.Key.Item.ID + OriginalContainerIndex = + Item.ItemList.Where(it => it.Submarine == validContainer.Key.Item.Submarine && it.OriginalModuleIndex == validContainer.Key.Item.OriginalModuleIndex).ToList().IndexOf(validContainer.Key.Item) }; foreach (WifiComponent wifiComponent in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index cb4ec388a..d55314017 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -46,6 +46,7 @@ namespace Barotrauma public List ItemsInBuyCrate { get; } = new List(); public List ItemsInSellCrate { get; } = new List(); + public List ItemsInSellFromSubCrate { get; } = new List(); public List PurchasedItems { get; } = new List(); public List SoldItems { get; } = new List(); @@ -55,6 +56,7 @@ namespace Barotrauma public Action OnItemsInBuyCrateChanged; public Action OnItemsInSellCrateChanged; + public Action OnItemsInSellFromSubCrateChanged; public Action OnPurchasedItemsChanged; public Action OnSoldItemsChanged; @@ -75,6 +77,12 @@ namespace Barotrauma OnItemsInSellCrateChanged?.Invoke(); } + public void ClearItemsInSellFromSubCrate() + { + ItemsInSellFromSubCrate.Clear(); + OnItemsInSellFromSubCrateChanged?.Invoke(); + } + public void SetPurchasedItems(List items) { PurchasedItems.Clear(); @@ -246,42 +254,21 @@ namespace Barotrauma continue; } availableContainers.Add(itemContainer); - #if SERVER +#if SERVER if (GameMain.Server != null) { Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); } - #endif - } - } - - if (itemContainer == null) - { - //no container, place at the waypoint - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine, onSpawned: itemSpawned); +#endif } - else - { - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemSpawned(item); - } - continue; - } - - //place in the container - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory, onSpawned: itemSpawned); - } - else - { - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemContainer.Inventory.TryPutItem(item, null); - itemSpawned(item); } + var item = new Item(pi.ItemPrefab, position, wp.Submarine); + itemContainer?.Inventory.TryPutItem(item, null); + itemSpawned(item); +#if SERVER + Entity.Spawner?.CreateNetworkEvent(item, false); +#endif static void itemSpawned(Item item) { Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 0c9cf2706..8eed7fcfa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -30,6 +31,8 @@ namespace Barotrauma public ReadyCheck ActiveReadyCheck; + public XElement ActiveOrdersElement { get; set; } + public CrewManager(bool isSinglePlayer) { IsSinglePlayer = isSinglePlayer; @@ -111,6 +114,9 @@ namespace Barotrauma case "health": characterInfo.HealthData = subElement; break; + case "orders": + characterInfo.OrderData = subElement; + break; } } } @@ -189,7 +195,7 @@ namespace Barotrauma spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && - wp.CurrentHull?.OutpostModuleTags != null && + wp.CurrentHull != null && wp.CurrentHull.OutpostModuleTags.Contains("airlock")); while (spawnWaypoints.Count > characterInfos.Count) { @@ -229,10 +235,14 @@ namespace Barotrauma } if (character.Info.HealthData != null) { - character.Info.ApplyHealthData(character, character.Info.HealthData); + CharacterInfo.ApplyHealthData(character, character.Info.HealthData); } character.GiveIdCardTags(spawnWaypoints[i]); character.Info.StartItemsGiven = true; + if (character.Info.OrderData != null) + { + character.Info.ApplyOrderData(); + } } AddCharacter(character); @@ -265,6 +275,14 @@ namespace Barotrauma RemoveCharacterInfo(characterInfo); } + public void ClearCurrentOrders() + { + foreach (var characterInfo in characterInfos) + { + characterInfo?.ClearCurrentOrders(); + } + } + public void Update(float deltaTime) { foreach (Pair order in ActiveOrders) @@ -392,6 +410,88 @@ namespace Barotrauma #endregion + public static Character GetCharacterForQuickAssignment(Order order, Character controlledCharacter, IEnumerable characters, bool includeSelf = false) + { + bool isControlledCharacterNull = controlledCharacter == null; +#if !DEBUG + if (isControlledCharacterNull) { return null; } +#endif + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemTargetedBySomeone(order.TargetItemComponent, controlledCharacter != null ? controlledCharacter.TeamID : CharacterTeamType.Team1, out Character operatingCharacter) && + (isControlledCharacterNull || operatingCharacter.CanHearCharacter(controlledCharacter))) + { + return operatingCharacter; + } + return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => isControlledCharacterNull || c.CanHearCharacter(controlledCharacter)) ?? controlledCharacter; + } + + public static IEnumerable GetCharactersSortedForOrder(Order order, IEnumerable characters, Character controlledCharacter, bool includeSelf, IEnumerable extraCharacters = null) + { + var filteredCharacters = characters.Where(c => controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID)); + if (extraCharacters != null) + { + filteredCharacters = filteredCharacters.Union(extraCharacters); + } + return filteredCharacters + // 1. Prioritize those who are on the same submarine than the controlled character + .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) + // 2. Prioritize those who have been given the same maintenance or operate order as now issued + .ThenByDescending(c => c.CurrentOrders.Any(o => + o.Order != null && o.Order.Identifier == order.Identifier && + (order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate))) + // 3. Prioritize those with the appropriate job for the order + .ThenByDescending(c => order.HasAppropriateJob(c)) + // 4. Prioritize bots over player controlled characters + .ThenByDescending(c => c.IsBot) + // 5. Use the priority value of the current objective + .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) + // 6. Prioritize those with the best skill for the order + .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); + } + partial void UpdateProjectSpecific(float deltaTime); + + private void SaveActiveOrders(XElement parentElement) + { + ActiveOrdersElement = new XElement("activeorders"); + // Only save orders with no fade out time (e.g. ignore orders) + var ordersToSave = new List(); + foreach (var activeOrder in ActiveOrders) + { + var order = activeOrder?.First; + if (order == null || activeOrder.Second.HasValue) { continue; } + ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority)); + } + CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray()); + parentElement?.Add(ActiveOrdersElement); + } + + public void LoadActiveOrders() + { + if (ActiveOrdersElement == null) { return; } + foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement)) + { + IIgnorable ignoreTarget = null; + if (orderInfo.Order.IsIgnoreOrder) + { + switch (orderInfo.Order.TargetType) + { + case Order.OrderTargetType.Entity: + ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable; + break; + case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue: + ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable; + break; + default: + DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target"); + continue; + } + } + if (ignoreTarget != null) + { + ignoreTarget.OrderedToBeIgnored = true; + } + AddOrder(orderInfo.Order, null); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a52601939..0ead39f9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -17,25 +17,33 @@ namespace Barotrauma // Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down public static CampaignSettings Unsure = Empty; public bool RadiationEnabled { get; set; } + public int MaxMissionCount { get; set; } + + public const int DefaultMaxMissionCount = 2; + public const int MaxMissionCountLimit = 10; + public const int MinMissionCountLimit = 1; public CampaignSettings(IReadMessage inc) { RadiationEnabled = inc.ReadBoolean(); + MaxMissionCount = inc.ReadInt32(); } public CampaignSettings(XElement element) { RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true); + MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLower(), DefaultMaxMissionCount); } public void Serialize(IWriteMessage msg) { msg.Write(RadiationEnabled); + msg.Write(MaxMissionCount); } public XElement Save() { - return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled)); + return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLower().ToLower(), MaxMissionCount)); } } @@ -49,9 +57,9 @@ namespace Barotrauma //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 - const float FirstRoundEventDelay = 30.0f; + const float FirstRoundEventDelay = 0.0f; - public enum InteractionType { None, Talk, Map, Crew, Store, Repair, Upgrade, PurchaseSub } + public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub } public readonly CargoManager CargoManager; public UpgradeManager UpgradeManager; @@ -113,12 +121,15 @@ namespace Barotrauma { get { - if (Map.CurrentLocation?.SelectedMission != null) + if (Map.CurrentLocation != null) { - if (Map.CurrentLocation.SelectedMission.Locations[0] == Map.CurrentLocation.SelectedMission.Locations[1] || - Map.CurrentLocation.SelectedMission.Locations.Contains(Map.SelectedLocation)) + foreach (Mission mission in map.CurrentLocation.SelectedMissions) { - yield return Map.CurrentLocation.SelectedMission; + if (mission.Locations[0] == mission.Locations[1] || + mission.Locations.Contains(Map.SelectedLocation)) + { + yield return mission; + } } } foreach (Mission mission in extraMissions) @@ -169,7 +180,7 @@ namespace Barotrauma return Submarine.Loaded.FindAll(sub => sub != leavingSub && !leavingSub.DockedTo.Contains(sub) && - sub.Info.Type == SubmarineType.Player && + sub.Info.Type == SubmarineType.Player && sub.TeamID == CharacterTeamType.Team1 && // pirate subs are currently tagged as player subs as well sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && (sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit)); } @@ -240,23 +251,24 @@ namespace Barotrauma if (levelData.Type == LevelData.LevelType.Outpost) { //if there's an available mission that takes place in the outpost, select it - var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation); - if (availableMissionsInLocation.Any()) + foreach (var availableMission in currentLocation.AvailableMissions) { - currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault(); - } - else - { - currentLocation.SelectedMission = null; + if (availableMission.Locations[0] == currentLocation && availableMission.Locations[1] == currentLocation) + { + currentLocation.SelectMission(availableMission); + } } } else { - //if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost - if (currentLocation.SelectedMission?.Locations[0] == currentLocation && - currentLocation.SelectedMission?.Locations[1] == currentLocation) + foreach (Mission mission in currentLocation.SelectedMissions.ToList()) { - currentLocation.SelectedMission = null; + //if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost + if (mission.Locations[0] == currentLocation && + mission.Locations[1] == currentLocation) + { + currentLocation.DeselectMission(mission); + } } if (levelData.HasBeaconStation && !levelData.IsBeaconActive) @@ -268,7 +280,7 @@ namespace Barotrauma var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand); if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) { - extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } } @@ -282,10 +294,10 @@ namespace Barotrauma else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand); + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)Math.Max(p.Commonness, 0.1f)).ToList(), rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { - extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); } } } @@ -490,7 +502,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } @@ -518,7 +530,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } @@ -554,12 +566,14 @@ namespace Barotrauma { CargoManager.ClearItemsInBuyCrate(); CargoManager.ClearItemsInSellCrate(); + CargoManager.ClearItemsInSellFromSubCrate(); } else { if (GameMain.NetworkMember.IsServer) { CargoManager?.ClearItemsInBuyCrate(); + // TODO: CargoManager?.ClearItemsInSellFromSubCrate(); } else if (GameMain.NetworkMember.IsClient) { @@ -585,7 +599,7 @@ namespace Barotrauma if (c.IsDead) { CrewManager.RemoveCharacterInfo(c.Info); - c.DespawnNow(); + c.DespawnNow(createNetworkEvents: false); } } @@ -595,7 +609,6 @@ namespace Barotrauma { CrewManager.RemoveCharacterInfo(ci); } - ci?.ClearCurrentOrders(); } foreach (DockingPort port in DockingPort.List) @@ -637,6 +650,7 @@ namespace Barotrauma location.CreateStore(force: true); location.ClearMissions(); location.Discovered = false; + location.LevelData?.EventHistory?.Clear(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); @@ -851,11 +865,14 @@ namespace Barotrauma DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White); } } - - if (map.CurrentLocation?.SelectedMission != null) + + if (map.CurrentLocation != null) { - DebugConsole.NewMessage(" Selected mission: " + map.CurrentLocation.SelectedMission.Name, Color.White); - DebugConsole.NewMessage("\n" + map.CurrentLocation.SelectedMission.Description, Color.White); + foreach (Mission mission in map.CurrentLocation.SelectedMissions) + { + DebugConsole.NewMessage(" Selected mission: " + mission.Name, Color.White); + DebugConsole.NewMessage("\n" + mission.Description, Color.White); + } } } @@ -865,5 +882,25 @@ namespace Barotrauma map?.Remove(); map = null; } + + public int NumberOfMissionsAtLocation(Location location) + { + return Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location)); + } + + public void CheckTooManyMissions(Location currentLocation, Client sender) + { + foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation))) + { + if (NumberOfMissionsAtLocation(location) > Settings.MaxMissionCount) + { + DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions."); + foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.MaxMissionCount).ToList()) + { + currentLocation.DeselectMission(mission); + } + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs index ce1d009ac..0929c3be8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs @@ -26,6 +26,7 @@ namespace Barotrauma private XElement itemData; private XElement healthData; + public XElement OrderData { get; private set; } partial void InitProjSpecific(Client client); public CharacterCampaignData(Client client) @@ -38,8 +39,10 @@ namespace Barotrauma if (client.Character.Inventory != null) { itemData = new XElement("inventory"); - client.Character.SaveInventory(client.Character.Inventory, itemData); + Character.SaveInventory(client.Character.Inventory, itemData); } + OrderData = new XElement("orders"); + CharacterInfo.SaveOrderData(client.Character.Info, OrderData); } public CharacterCampaignData(XElement element) @@ -67,6 +70,9 @@ namespace Barotrauma case "health": healthData = subElement; break; + case "orders": + OrderData = subElement; + break; } } } @@ -78,8 +84,10 @@ namespace Barotrauma if (character.Inventory != null) { itemData = new XElement("inventory"); - character.SaveInventory(character.Inventory, itemData); + Character.SaveInventory(character.Inventory, itemData); } + OrderData = new XElement("orders"); + CharacterInfo.SaveOrderData(character.Info, OrderData); } public XElement Save() @@ -92,6 +100,7 @@ namespace Barotrauma CharacterInfo?.Save(element); if (itemData != null) { element.Add(itemData); } if (healthData != null) { element.Add(healthData); } + if (OrderData != null) { element.Add(OrderData); } return element; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index 8caf39c4f..d8871d2bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -21,7 +21,7 @@ namespace Barotrauma Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; foreach (MissionPrefab missionPrefab in missionPrefabs) { - missions.Add(missionPrefab.Instantiate(locations)); + missions.Add(missionPrefab.Instantiate(locations, Submarine.MainSub)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 6bbfa5cfb..b5bb43eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -149,12 +149,14 @@ namespace Barotrauma case "metadata": CampaignMetadata = new CampaignMetadata(this, subElement); break; + case "upgrademanager": case "pendingupgrades": UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: false); break; case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer: CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false); CrewManager.AddCharacterElements(subElement); + CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "cargo": CargoManager?.LoadPurchasedItems(subElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 6b6e4b57b..6b1f29e91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -162,6 +162,18 @@ namespace Barotrauma private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable missionPrefabs = null, MissionType missionType = MissionType.None) { + if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode)) + { + //don't allow hidden mission types (e.g. GoTo) in single mission modes + var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); + for (int i = 0; i < missionTypes.Length; i++) + { + if (MissionPrefab.HiddenMissionClasses.Contains(missionTypes[i])) + { + missionType &= ~missionTypes[i]; + } + } + } if (gameModePreset.GameModeType == typeof(CoOpMode)) { return missionPrefabs != null ? @@ -353,9 +365,20 @@ namespace Barotrauma } } } - if (GameMode is PvPMode && Submarine.MainSubs[1] == null) + + foreach (Mission mission in GameMode.Missions) { - Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); + // setting difficulty for missions that may involve difficulty-related submarine creation + mission.SetDifficulty(levelData?.Difficulty ?? 0f); + } + + if (Submarine.MainSubs[1] == null) + { + var enemySubmarineInfo = GameMode is PvPMode ? SubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo; + if (enemySubmarineInfo != null) + { + Submarine.MainSubs[1] = new Submarine(enemySubmarineInfo, true); + } } if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false) @@ -526,7 +549,10 @@ namespace Barotrauma if (port.IsHorizontal || port.Docked) { continue; } if (port.Item.Submarine == level.StartOutpost) { - outPostPort = port; + if (port.DockingTarget == null) + { + outPostPort = port; + } continue; } if (port.Item.Submarine != Submarine) { continue; } @@ -614,6 +640,18 @@ namespace Barotrauma return missions.IndexOf(mission); } + public void EnforceMissionOrder(List missionIdentifiers) + { + List sortedMissions = new List(); + foreach (string missionId in missionIdentifiers) + { + var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId); + sortedMissions.Add(matchingMission); + missions.Remove(matchingMission); + } + missions.AddRange(sortedMissions); + } + partial void UpdateProjSpecific(float deltaTime); public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index fae47582a..9148c6102 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -28,6 +28,18 @@ namespace Barotrauma } } + internal class PurchasedItemSwap + { + public readonly Item ItemToRemove; + public readonly ItemPrefab ItemToInstall; + + public PurchasedItemSwap(Item itemToRemove, ItemPrefab itemToInstall) + { + ItemToRemove = itemToRemove; + ItemToInstall = itemToInstall; + } + } + /// /// This class handles all upgrade logic. /// Storing, applying, checking and validation of upgrades. @@ -75,9 +87,10 @@ namespace Barotrauma public readonly List PendingUpgrades = new List(); + public readonly List PurchasedItemSwaps = new List(); + private CampaignMetadata Metadata => Campaign.CampaignMetadata; private readonly CampaignMode Campaign; - private int spentMoney; public event Action? OnUpgradesChanged; @@ -90,7 +103,67 @@ namespace Barotrauma public UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer) : this(campaign) { DebugConsole.Log($"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades)."); - LoadPendingUpgrades(element, isSingleplayer); + + //backwards compatibility: + //upgrades used to be saved to a element, now upgrades and item swaps are saved separately under a element + if (element.Name.LocalName.Equals("pendingupgrades", StringComparison.OrdinalIgnoreCase)) + { + LoadPendingUpgrades(element, isSingleplayer); + } + else + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "pendingupgrades": + LoadPendingUpgrades(subElement, isSingleplayer); + break; + } + } + } + } + + public int DetermineItemSwapCost(Item item, ItemPrefab? replacement) + { + if (replacement == null) + { + replacement = ItemPrefab.Find("", item.Prefab.SwappableItem.ReplacementOnUninstall); + if (replacement == null) + { + DebugConsole.ThrowError("Failed to determine swap cost for item \"{}\". Trying to uninstall the item but no replacement item found."); + return 0; + } + } + + int price = 0; + if (replacement == item.Prefab) + { + if (item.PendingItemSwap != null) + { + //refund the pending swap + price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + //buy back the current item + price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + } + } + else + { + price = replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + if (item.PendingItemSwap != null) + { + //refund the pending swap + price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + //buy back the current item + price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + } + //refund the current item + if (replacement != item.prefab) + { + price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); + } + } + return price; } private DateTime lastUpgradeSpeak, lastErrorSpeak; @@ -102,9 +175,6 @@ namespace Barotrauma /// Purchased upgrades are temporarily stored in and they are applied /// after the next round starts similarly how items are spawned in the stowage room after the round starts. /// - /// - /// - /// public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false) { if (!CanUpgradeSub()) @@ -142,7 +212,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money > price) + if (Campaign.Money >= price) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -155,7 +225,6 @@ namespace Barotrauma } Campaign.Money -= price; - spentMoney += price; PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); @@ -184,6 +253,159 @@ namespace Barotrauma } } + /// + /// Purchases an item swap and handles logic for deducting the credit. + /// + public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false) + { + if (!CanUpgradeSub()) + { + DebugConsole.ThrowError("Cannot swap items when switching to another submarine."); + return; + } + if (itemToRemove == null) + { + DebugConsole.ThrowError($"Cannot swap null item!"); + return; + } + if (itemToRemove.HiddenInGame) + { + DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's set to be hidden in-game."); + return; + } + if (!itemToRemove.AllowSwapping) + { + DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's configured to be non-swappable."); + return; + } + if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.HasTag(t)) && c.ItemTags.Any(t => itemToInstall.Tags.Contains(t)))) + { + DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category)."); + return; + } + + if (itemToRemove.prefab == itemToInstall) + { + DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!)."); + return; + } + SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem; + if (swappableItem == null) + { + DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (not configured as a swappable item)."); + return; + } + + int price = 0; + if (!itemToRemove.AvailableSwaps.Contains(itemToInstall)) + { + price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation); + } + + if (force) + { + price = 0; + } + + if (Campaign.Money >= price) + { + PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove); + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + // only make the NPC speak if more than 5 minutes have passed since the last purchased service + if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) + { + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + lastUpgradeSpeak = DateTime.Now; + } + } + + Campaign.Money -= price; + + itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab); + if (itemToInstall != null && !itemToRemove.AvailableSwaps.Contains(itemToInstall)) + { + itemToRemove.PurchasedNewSwap = true; + itemToRemove.AvailableSwaps.Add(itemToInstall); + } + + if (itemToRemove.Prefab != itemToInstall && itemToInstall != null) + { + itemToRemove.PendingItemSwap = itemToInstall; + PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); + DebugLog($"CLIENT: Swapped item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\".", Color.Orange); + } + else + { + DebugLog($"CLIENT: Cancelled swapping the item \"{itemToRemove.Name}\" with \"{(itemToRemove.PendingItemSwap?.Name ?? null)}\".", Color.Orange); + } + OnUpgradesChanged?.Invoke(); + } + else + { + DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" + + $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}"); + } + } + + /// + /// Cancels the currently pending item swap, or uninstalls the item if there's no swap pending + /// + public void CancelItemSwap(Item itemToRemove, bool force = false) + { + if (!CanUpgradeSub()) + { + DebugConsole.ThrowError("Cannot swap items when switching to another submarine."); + return; + } + + if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall)) + { + DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured)."); + return; + } + + SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem; + if (swappableItem == null) + { + DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\" (not configured as a swappable item)."); + return; + } + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + // only make the NPC speak if more than 5 minutes have passed since the last purchased service + if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) + { + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + lastUpgradeSpeak = DateTime.Now; + } + } + + if (itemToRemove.PendingItemSwap == null) + { + var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab; + if (replacement == null) + { + DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\"."); + return; + } + PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove); + PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, replacement)); + DebugLog($"Uninstalled item item \"{itemToRemove.Name}\".", Color.Orange); + itemToRemove.PendingItemSwap = replacement; + } + else + { + PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove); + DebugLog($"Cancelled swapping the item \"{itemToRemove.Name}\" with \"{itemToRemove.PendingItemSwap.Name}\".", Color.Orange); + itemToRemove.PendingItemSwap = null; + } +#if CLIENT + OnUpgradesChanged?.Invoke(); +#endif + } + /// /// Applies all our pending upgrades to the submarine. /// @@ -201,24 +423,22 @@ namespace Barotrauma public void ApplyUpgrades() { PurchasedUpgrades.Clear(); + PurchasedItemSwaps.Clear(); if (Submarine.MainSub == null) { return; } List pendingUpgrades = PendingUpgrades; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + if (Level.Loaded is { Type: LevelData.LevelType.Outpost }) { - if (Level.Loaded?.Type != LevelData.LevelType.Outpost) + return; + } + + if (GameMain.NetworkMember is { IsClient: true }) + { + if (loadedUpgrades != null) { - if (loadedUpgrades != null) - { - // client receives pending upgrades from the save file - pendingUpgrades = loadedUpgrades; - } - } - else - { - // prevent the client from applying pending upgrades at an outpost when joining mid round - return; + // client receives pending upgrades from the save file + pendingUpgrades = loadedUpgrades; } } @@ -227,36 +447,12 @@ namespace Barotrauma { int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level); DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})"); - if (newLevel > 0) - { - SetUpgradeLevel(prefab, category, Math.Clamp(newLevel, 0, prefab.MaxLevel)); - } + SetUpgradeLevel(prefab, category, Math.Clamp(GetRealUpgradeLevel(prefab, category) + level, 0, prefab.MaxLevel)); } PendingUpgrades.Clear(); loadedUpgrades?.Clear(); loadedUpgrades = null; - spentMoney = 0; - } - - /// - /// Cancels the pending upgrades and refunds the money spent - /// - private void RefundUpgrades() - { - DebugConsole.Log($"Refunded {spentMoney} marks in pending upgrades."); - if (spentMoney > 0) - { -#if CLIENT - GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("UpgradeRefundTitle"), TextManager.Get("UpgradeRefundBody"), new[] { TextManager.Get("Ok") }); - msgBox.Buttons[0].OnClicked += msgBox.Close; -#endif - } - - Campaign.Money += spentMoney; - spentMoney = 0; - PendingUpgrades.Clear(); - PurchasedUpgrades.Clear(); } public void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character) @@ -314,7 +510,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugConsole.AddWarning($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); + DebugLog($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); FixUpgradeOnItem(wall, prefab, level); } } @@ -343,7 +539,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugConsole.AddWarning($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); + DebugLog($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); FixUpgradeOnItem(item, prefab, level); } } @@ -486,113 +682,17 @@ namespace Barotrauma return Campaign.PendingSubmarineSwitch == null; } - public void RefundResetAndReload(SubmarineInfo newSubmarine, bool notifyClients = false) + public void Save(XElement? parent) { - RefundUpgrades(); - ResetUpgrades(); - Dictionary newUpgrades = ReloadUpgradeValues(newSubmarine); -#if SERVER - if (notifyClients) - { - SendUpgradeResetMessage(newUpgrades); - } -#endif + if (parent == null) { return; } + + var upgradeManagerElement = new XElement("upgrademanager"); + parent.Add(upgradeManagerElement); + + SavePendingUpgrades(upgradeManagerElement, PendingUpgrades); } - /// - /// Parses a SubmarineInfo and sets the store values accordingly. - /// Used when reloading a previously saved submarine. - /// - /// - private Dictionary ReloadUpgradeValues(SubmarineInfo info) - { - Dictionary newValues = new Dictionary(); - IEnumerable linkedSubElements = info.SubmarineElement.Elements().Where(element => element.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase)).SelectMany(element => element.Elements()); - IEnumerable mainSubElements = info.SubmarineElement.Elements().Where(Predicate); - List elements = mainSubElements.Concat(linkedSubElements.Where(Predicate)).ToList(); - foreach (UpgradeCategory category in UpgradeCategory.Categories) - { - foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) - { - if (!prefab.UpgradeCategories.Contains(category)) { continue; } - - List levels = GetUpgradeFromXML(elements, category, prefab); - if (levels.Any()) - { - int level = (int) levels.Average(i => i); - newValues.Add(FormatIdentifier(prefab, category), level); - } - } - } - - foreach (var (dataIdentifier, level) in newValues) - { - Campaign.CampaignMetadata.SetValue(dataIdentifier, level); - } - - return newValues; - - static List GetUpgradeFromXML(List elements, UpgradeCategory category, UpgradePrefab prefab) - { - List levels = new List(); - foreach (XElement subElement in elements) - { - if (!category.CanBeApplied(subElement, prefab)) { continue; } - - foreach (XElement component in subElement.Elements()) - { - if (string.Equals(component.Name.ToString(), "upgrade", StringComparison.OrdinalIgnoreCase)) - { - string identifier = component.GetAttributeString("identifier", string.Empty); - int level = component.GetAttributeInt("level", -1); - if (string.IsNullOrWhiteSpace(identifier) || level <= 0) { continue; } - - UpgradePrefab? matchingPrefab = UpgradePrefab.Find(identifier); - if (matchingPrefab == null || matchingPrefab != prefab) { continue; } - - if (matchingPrefab.UpgradeCategories.Contains(category)) { levels.Add(level); } - } - } - } - - return levels; - } - - static bool Predicate(XElement element) => element.HasElements && element.Elements().Any(e => e.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)); - } - - - /// - /// Resets our upgrade progress and prices. - /// This does not actually remove the upgrades from the submarine but resets the store interface. - /// - /// - /// This method works by iterating thru all upgrade categories and prefabs and checking if they have a - /// valid key stored in the metadata, if they do set it to 0, upgrades without a key stored are always - /// assumed to be 0 so they don't need to be reset. - /// - /// Should initially be called server side as we can't trust clients with such a simple notification. - /// - private void ResetUpgrades() - { - foreach (UpgradeCategory category in UpgradeCategory.Categories) - { - foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) - { - if (!prefab.UpgradeCategories.Contains(category)) { continue; } - - string dataIdentifier = FormatIdentifier(prefab, category); - if (Metadata.HasKey(dataIdentifier)) - { - Metadata.SetValue(dataIdentifier, 0); - } - } - } - - OnUpgradesChanged?.Invoke(); - } - - public void SavePendingUpgrades(XElement? parent, List upgrades) + private void SavePendingUpgrades(XElement? parent, List upgrades) { if (parent == null) { return; } @@ -612,7 +712,7 @@ namespace Barotrauma private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true) { - if (element == null || !element.HasElements) { return; } + if (!(element is { HasElements: true })) { return; } List pendingUpgrades = new List(); @@ -647,7 +747,7 @@ namespace Barotrauma #endif } - public static void LogError(string text, Dictionary data, Exception e = null) + public static void LogError(string text, Dictionary data, Exception? e = null) { string error = $"{text}\n"; foreach (var (label, value) in data) @@ -658,34 +758,10 @@ namespace Barotrauma DebugConsole.ThrowError(error.TrimEnd('\n'), e); } - public static Dictionary GetMetadataLevels(CampaignMetadata? metadata) - { - Dictionary values = new Dictionary(); - - if (metadata == null) { return values; } - - foreach (UpgradeCategory category in UpgradeCategory.Categories) - { - foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) - { - string identifier = FormatIdentifier(prefab, category); - if (metadata.HasKey(identifier) && !values.ContainsKey(identifier)) - { - values.Add(identifier, metadata.GetInt(identifier)); - } - } - } - - return values; - } - /// /// Used to sync the pending upgrades list in multiplayer. /// /// - /// - /// In singleplayer this is not used and should not be. - /// public void SetPendingUpgrades(List upgrades) { PendingUpgrades.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 2348315d4..d5cc99aea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -616,7 +616,8 @@ namespace Barotrauma f.Type == ContentType.Outpost || f.Type == ContentType.OutpostModule || f.Type == ContentType.Wreck || - f.Type == ContentType.BeaconStation)) { SubmarineInfo.RefreshSavedSubs(); } + f.Type == ContentType.BeaconStation || + f.Type == ContentType.EnemySubmarine)) { SubmarineInfo.RefreshSavedSubs(); } if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); } if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); } if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 7e2666efa..a72c111f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -114,6 +114,17 @@ namespace Barotrauma public bool IsInLimbSlot(Item item, InvSlotType limbSlot) { + if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand)) + { + int rightHandSlot = FindLimbSlot(InvSlotType.RightHand); + int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand); + if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) && + leftHandSlot > -1 && slots[leftHandSlot].Contains(item)) + { + return true; + } + } + for (int i = 0; i < slots.Length; i++) { if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; } @@ -205,13 +216,41 @@ namespace Barotrauma if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any)) { - int slot = FindLimbSlot(allowedSlots.First()); - if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item)) + bool allSlotsTaken = true; + foreach (var allowedSlot in allowedSlots) { - foreach (Item existingItem in slots[slot].Items.ToList()) + if (allowedSlot == (InvSlotType.RightHand | InvSlotType.LeftHand)) { - existingItem.Drop(user); - if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); } + int rightHandSlot = FindLimbSlot(InvSlotType.RightHand); + int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand); + if (rightHandSlot > -1 && slots[rightHandSlot].CanBePut(item) && + leftHandSlot > -1 && slots[leftHandSlot].CanBePut(item)) + { + allSlotsTaken = false; + break; + } + } + else + { + int slot = FindLimbSlot(allowedSlot); + if (slot > -1 && slots[slot].CanBePut(item)) + { + allSlotsTaken = false; + break; + } + } + + } + if (allSlotsTaken) + { + int slot = FindLimbSlot(allowedSlots.First()); + if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item)) + { + foreach (Item existingItem in slots[slot].Items.ToList()) + { + existingItem.Drop(user); + if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); } + } } } } @@ -304,7 +343,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty()) + if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty()) { #if CLIENT if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } @@ -390,7 +429,7 @@ namespace Barotrauma if (SlotTypes[index] == InvSlotType.Any) { - if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; } + if (!item.GetComponents().Any(p => p.AllowedSlots.Contains(InvSlotType.Any))) { return false; } if (slots[index].Any()) { return slots[index].Contains(item); } PutItem(item, index, user, true, createNetworkEvent); return true; @@ -399,20 +438,23 @@ namespace Barotrauma InvSlotType placeToSlots = InvSlotType.None; bool slotsFree = true; - foreach (InvSlotType allowedSlot in item.AllowedSlots) + foreach (Pickable pickable in item.GetComponents()) { - if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } -#if CLIENT - if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } -#endif - for (int i = 0; i < capacity; i++) + foreach (InvSlotType allowedSlot in pickable.AllowedSlots) { - if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) + if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } + #if CLIENT + if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } + #endif + for (int i = 0; i < capacity; i++) { - slotsFree = false; - break; + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) + { + slotsFree = false; + break; + } + placeToSlots = allowedSlot; } - placeToSlots = allowedSlot; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index dabd8d959..974921b4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -538,16 +538,18 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < 2; i++) { - if (hull.Submarine != subs[i]) continue; - if (hull.WorldRect.Y < hullRects[i].Y - hullRects[i].Height) continue; - if (hull.WorldRect.Y - hull.WorldRect.Height > hullRects[i].Y) continue; + if (hull.Submarine != subs[i]) { continue; } + if (hull.WorldRect.Y - 5 < hullRects[i].Y - hullRects[i].Height) { continue; } + if (hull.WorldRect.Y - hull.WorldRect.Height + 5 > hullRects[i].Y) { continue; } if (i == 0) //left hull { + if (hull.WorldPosition.X > hullRects[0].Center.X) { continue; } leftSubRightSide = Math.Max(hull.WorldRect.Right, leftSubRightSide); } else //upper hull { + if (hull.WorldPosition.X < hullRects[1].Center.X) { continue; } rightSubLeftSide = Math.Min(hull.WorldRect.X, rightSubLeftSide); } } @@ -591,8 +593,11 @@ namespace Barotrauma.Items.Components } } + int expand = 5; for (int i = 0; i < 2; i++) { + hullRects[i].X -= expand; + hullRects[i].Width += expand * 2; hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); @@ -636,16 +641,18 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < 2; i++) { - if (hull.Submarine != subs[i]) continue; - if (hull.WorldRect.Right < hullRects[i].X) continue; - if (hull.WorldRect.X > hullRects[i].Right) continue; + if (hull.Submarine != subs[i]) { continue; } + if (hull.WorldRect.Right - 5 < hullRects[i].X) { continue; } + if (hull.WorldRect.X + 5 > hullRects[i].Right) { continue; } if (i == 0) //lower hull { + if (hull.WorldPosition.Y > hullRects[i].Y - hullRects[i].Height / 2) { continue; } lowerSubTop = Math.Max(hull.WorldRect.Y, lowerSubTop); } else //upper hull { + if (hull.WorldPosition.Y < hullRects[i].Y - hullRects[i].Height / 2) { continue; } upperSubBottom = Math.Min(hull.WorldRect.Y - hull.WorldRect.Height, upperSubBottom); } } @@ -705,8 +712,11 @@ namespace Barotrauma.Items.Components } + int expand = 5; for (int i = 0; i < 2; i++) { + hullRects[i].Y += expand; + hullRects[i].Height += expand * 2; hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); @@ -797,22 +807,22 @@ namespace Barotrauma.Items.Components for (int i = 0; i < 2; i++) { Gap doorGap = i == 0 ? Door?.LinkedGap : DockingTarget?.Door?.LinkedGap; - if (doorGap == null) continue; + if (doorGap == null) { continue; } doorGap.DisableHullRechecks = true; - if (doorGap.linkedTo.Count >= 2) continue; + if (doorGap.linkedTo.Count >= 2) { continue; } if (IsHorizontal) { if (item.WorldPosition.X < DockingTarget.item.WorldPosition.X) { - if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]); + if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); } } else { - if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]); + if (!doorGap.linkedTo.Contains(hulls[1])) { doorGap.linkedTo.Add(hulls[1]); } } //make sure the left hull is linked to the gap first (gap logic assumes that the first hull is the one to the left) - if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].Rect.X > doorGap.linkedTo[1].Rect.X) + if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].WorldRect.X > doorGap.linkedTo[1].WorldRect.X) { var temp = doorGap.linkedTo[0]; doorGap.linkedTo[0] = doorGap.linkedTo[1]; @@ -821,16 +831,16 @@ namespace Barotrauma.Items.Components } else { - if (item.WorldPosition.Y < DockingTarget.item.WorldPosition.Y) + if (item.WorldPosition.Y > DockingTarget.item.WorldPosition.Y) { - if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]); + if (!doorGap.linkedTo.Contains(hulls[0])) { doorGap.linkedTo.Add(hulls[0]); } } else { - if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]); + if (!doorGap.linkedTo.Contains(hulls[1])) { doorGap.linkedTo.Add(hulls[1]); } } //make sure the upper hull is linked to the gap first (gap logic assumes that the first hull is above the second one) - if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].Rect.Y < doorGap.linkedTo[1].Rect.Y) + if (doorGap.linkedTo.Count > 1 && doorGap.linkedTo[0].WorldRect.Y < doorGap.linkedTo[1].WorldRect.Y) { var temp = doorGap.linkedTo[0]; doorGap.linkedTo[0] = doorGap.linkedTo[1]; @@ -887,10 +897,7 @@ namespace Barotrauma.Items.Components } var wire = item.GetComponent(); - if (wire != null) - { - wire.Drop(null); - } + wire?.Drop(null); if (joint != null) { @@ -991,7 +998,7 @@ namespace Barotrauma.Items.Components { if (DockingTarget.Door != null && doorBody != null) { - doorBody.Enabled = DockingTarget.Door.Body.Enabled; + doorBody.Enabled = DockingTarget.Door.Body.Enabled && !(DockingTarget.Door.Body.FarseerBody.FixtureList.FirstOrDefault()?.IsSensor ?? false); } dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 528fd48a5..99927ca74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -259,6 +259,10 @@ namespace Barotrauma.Items.Components Body.SetTransformIgnoreContacts( ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)), 0.0f); + if (isBroken) + { + DisableBody(); + } } public override void Move(Vector2 amount) @@ -659,7 +663,7 @@ namespace Barotrauma.Items.Components } else { - return Item.GetConnectedComponents(true).Any(b => b.HasAccess(character)); + return base.HasAccess(character) && Item.GetConnectedComponents(true).Any(b => b.HasAccess(character)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 60b08dd4a..cbfc92b1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -319,6 +319,25 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { + //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa) + if (item.GetComponents().Count() > 0) + { + bool inSuitableSlot = false; + for (int i = 0; i < character.Inventory.Capacity; i++) + { + if (character.Inventory.GetItemsAt(i).Contains(item)) + { + if (character.Inventory.SlotTypes[i] != InvSlotType.Any && + allowedSlots.Any(a => a.HasFlag(character.Inventory.SlotTypes[i]))) + { + inSuitableSlot = true; + break; + } + } + } + if (!inSuitableSlot) { return; } + } + picker = character; if (item.Removed) @@ -327,6 +346,13 @@ namespace Barotrauma.Items.Components return; } + var wearable = item.GetComponent(); + if (wearable != null) + { + //cannot hold and wear an item at the same time + wearable.Unequip(character); + } + if (character != null) { item.Submarine = character.Submarine; } if (item.body == null) { @@ -414,7 +440,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null) { - return attachTargetCell != null && Structure.GetAttachTarget(item.WorldPosition) != null; + return attachTargetCell != null || Structure.GetAttachTarget(item.WorldPosition) != null; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index d3849f9bf..c45a4894d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -75,6 +75,14 @@ namespace Barotrauma.Items.Components IsActive = true; } + public override void Move(Vector2 amount) + { + if (trigger != null && amount.LengthSquared() > 0.00001f) + { + trigger.SetTransform(item.SimPosition, 0.0f); + } + } + public override void Update(float deltaTime, Camera cam) { if (holdable != null && !holdable.Attached) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index a8b797122..f59597c61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -132,7 +132,12 @@ namespace Barotrauma.Items.Components } return false; } - + + public override bool SecondaryUse(float deltaTime, Character character = null) + { + return characterUsable || character == null; + } + public override void Drop(Character dropper) { base.Drop(dropper); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index e0a31335b..dbac3d4fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -90,6 +90,24 @@ namespace Barotrauma.Items.Components public virtual bool OnPicked(Character picker) { + //if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa) + if (item.GetComponents().Count() > 0) + { + bool alreadyEquipped = false; + for (int i = 0; i < picker.Inventory.Capacity; i++) + { + if (picker.Inventory.GetItemsAt(i).Contains(item)) + { + if (picker.Inventory.SlotTypes[i] != InvSlotType.Any && + !allowedSlots.Any(a => a.HasFlag(picker.Inventory.SlotTypes[i]))) + { + alreadyEquipped = true; + break; + } + } + } + if (alreadyEquipped) { return false; } + } if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots)) { if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 21d31ac64..8a73e00a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -69,6 +69,7 @@ namespace Barotrauma.Items.Components item.IsShootable = true; // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; + characterUsable = true; InitProjSpecific(element); } @@ -151,6 +152,11 @@ namespace Barotrauma.Items.Components return true; } + public override bool SecondaryUse(float deltaTime, Character character = null) + { + return characterUsable || character == null; + } + public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { var containedItems = item.OwnInventory?.AllItemsMod; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 5ad4e89bf..3052da20c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -548,6 +548,11 @@ namespace Barotrauma.Items.Components } return true; } + else if (targetBody.UserData is LevelObject levelObject && levelObject.Prefab.TakeLevelWallDamage) + { + levelObject.AddDamage(-LevelWallFixAmount, deltaTime, item); + return true; + } else if (targetBody.UserData is Character targetCharacter) { if (targetCharacter.Removed) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index f6e4ce5bd..8365d479f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -660,6 +660,7 @@ namespace Barotrauma.Items.Components /// public virtual bool HasAccess(Character character) { + if (character.IsBot && item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (requiredItems.None()) { return true; } if (character.Inventory != null) @@ -678,7 +679,6 @@ namespace Barotrauma.Items.Components public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null) { if (requiredItems.None()) { return true; } - if (!character.IsPlayer && character.Params.AI != null && character.Params.AI.Infiltrate) { return true; } if (character.Inventory == null) { return false; } bool hasRequiredItems = false; bool canContinue = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 3c1ecfaa7..08d3d33ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components } } + private bool alwaysContainedItemsSpawned; + public ItemInventory Inventory; private readonly List activeContainedItems = new List(); @@ -204,8 +206,15 @@ namespace Barotrauma.Items.Components return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null; } + readonly List targets = new List(); + public override void Update(float deltaTime, Camera cam) { + if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) + { + SpawnAlwaysContainedItems(); + } + if (item.ParentInventory is CharacterInventory) { item.SetContainedItemPositions(); @@ -235,8 +244,8 @@ namespace Barotrauma.Items.Components if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - effect.GetNearbyTargets(item.WorldPosition, targets); + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets)); effect.Apply(ActionType.OnActive, deltaTime, item, targets); } } @@ -403,10 +412,28 @@ namespace Barotrauma.Items.Components { if (SpawnWithId.Length > 0) { - ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); - if (prefab != null && Inventory != null && Inventory.CanBePut(prefab)) + string[] splitIds = SpawnWithId.Split(','); + foreach (string id in splitIds) { - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false); + ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id); + if (prefab != null && Inventory != null && Inventory.CanBePut(prefab)) + { + bool isEditor = false; +#if CLIENT + isEditor = Screen.Selected == GameMain.SubEditorScreen; +#endif + if (!isEditor && (Entity.Spawner == null || Entity.Spawner.Removed) && GameMain.NetworkMember == null) + { + var spawnedItem = new Item(prefab, Vector2.Zero, null); + Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false); + alwaysContainedItemsSpawned = true; + } + else + { + IsActive = true; + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index e4502d68c..0e9457394 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -172,14 +172,23 @@ namespace Barotrauma.Items.Components float scaledDamageRange = propellerDamage.DamageRange * item.Scale; - Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale; + Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale; + float broadRange = Math.Max(scaledDamageRange * 2, 500); foreach (Character character in Character.CharacterList) { if (!character.Enabled || character.Removed) { continue; } - float distSqr = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos); - if (distSqr > scaledDamageRange * scaledDamageRange) { continue; } - character.LastDamageSource = item; - propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true); + if (Math.Abs(character.WorldPosition.X - propellerWorldPos.X) > broadRange) { continue; } + if (Math.Abs(character.WorldPosition.Y - propellerWorldPos.Y) > broadRange) { continue; } + + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.IsSevered || !limb.body.Enabled) { continue; } + float distSqr = Vector2.DistanceSquared(limb.WorldPosition, propellerWorldPos); + if (distSqr > scaledDamageRange * scaledDamageRange) { continue; } + character.LastDamageSource = item; + propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true); + break; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 7e19a9248..2080a95d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -290,10 +290,12 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < ingredient.Amount; i++) { - var availableItem = availableIngredients.FirstOrDefault(it => it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && it.ConditionPercentage >= ingredient.MinCondition * 100.0f); + var availableItem = availableIngredients.FirstOrDefault(it => + it != null && ingredient.ItemPrefabs.Contains(it.Prefab) && + it.ConditionPercentage >= ingredient.MinCondition * 100.0f && + it.ConditionPercentage <= ingredient.MaxCondition * 100.0f); if (availableItem == null) { continue; } - - //Item4 = use condition bool + if (ingredient.UseCondition && availableItem.ConditionPercentage - ingredient.MinCondition * 100 > 0.0f) //Leave it behind with reduced condition if it has enough to stay above 0 { availableItem.Condition -= availableItem.Prefab.Health * ingredient.MinCondition; @@ -493,7 +495,8 @@ namespace Barotrauma.Items.Components return item != null && requiredItem.ItemPrefabs.Contains(item.prefab) && - item.Condition / item.Prefab.Health >= requiredItem.MinCondition; + item.Condition / item.Prefab.Health >= requiredItem.MinCondition && + item.Condition / item.Prefab.Health <= requiredItem.MaxCondition; } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 6e0073d67..109645fc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -67,6 +67,9 @@ namespace Barotrauma.Items.Components public Character LastAIUser { get; private set; } + [Serialize(defaultValue: false, isSaveable: true)] + public bool LastUserWasPlayer { get; private set; } + private Character lastUser; public Character LastUser { @@ -178,8 +181,6 @@ namespace Barotrauma.Items.Components [Serialize(0.0f, true)] public float AvailableFuel { get; set; } - public bool LastUserWasPlayer { get; private set; } - public Reactor(Item item, XElement element) : base(item, element) { @@ -241,7 +242,7 @@ namespace Barotrauma.Items.Components optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); - + optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess); allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess); @@ -251,11 +252,13 @@ namespace Barotrauma.Items.Components allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10); float heatAmount = GetGeneratedHeat(fissionRate); + float temperatureDiff = (heatAmount - turbineOutput) - Temperature; Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff)); //if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime; - + FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime); + TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime); float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); @@ -311,6 +314,38 @@ namespace Barotrauma.Items.Components } } + if (!loadQueue.Any() && PowerOn) + { + //loadQueue is empty, round must've just started + //reset the fission rate, turbine output and + //temperature to optimal levels to prevent fires + //at the start of the round + correctTurbineOutput = currentLoad / MaxPowerOutput * 100.0f; + tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess); + optimalTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); + tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); + allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); + + DebugConsole.Log($"Degree of success: {degreeOfSuccess}"); + DebugConsole.Log($"Current load: {currentLoad}"); + DebugConsole.Log($"Max power output: {MaxPowerOutput}"); + DebugConsole.Log($"Available fuel: {AvailableFuel}"); + + float desiredTurbineOutput = MathHelper.Clamp(correctTurbineOutput, 0.0f, 100.0f); + DebugConsole.Log($"Turbine output reset: {targetTurbineOutput}, {turbineOutput} -> {desiredTurbineOutput}"); + targetTurbineOutput = desiredTurbineOutput; + turbineOutput = desiredTurbineOutput; + + float desiredTemperature = (optimalTemperature.X + optimalTemperature.Y) / 2.0f; + DebugConsole.Log($"Temperature reset: {temperature} -> {desiredTemperature}"); + temperature = desiredTemperature; + + float desiredFissionRate = GetFissionRateForTargetTemperatureAndTurbineOutput(desiredTemperature, desiredTurbineOutput); + DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}"); + targetFissionRate = desiredFissionRate; + fissionRate = desiredFissionRate; + } + loadQueue.Enqueue(currentLoad); while (loadQueue.Count() > 60.0f) { @@ -390,6 +425,12 @@ namespace Barotrauma.Items.Components return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; } + private float GetFissionRateForTargetTemperatureAndTurbineOutput(float temperature, float turbineOutput) + { + if (MathUtils.NearlyEqual(AvailableFuel, 0f)) { return 0f; } + return (temperature + turbineOutput) / (AvailableFuel / 100f) / 2f; + } + /// /// Do we need more fuel to generate enough power to match the current load. /// @@ -564,6 +605,7 @@ namespace Barotrauma.Items.Components public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + character.AIController.SteeringManager.Reset(); bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase); IsActive = true; @@ -598,6 +640,7 @@ namespace Barotrauma.Items.Components void ReportFuelRodCount() { if (!character.IsOnPlayerTeam) { return; } + if (character.Submarine != Submarine.MainSub) { return; } int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); if (remainingFuelRods == 0) { @@ -614,6 +657,18 @@ namespace Barotrauma.Items.Components } else { + if (Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(Item, character)) + { + if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f) + { + objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true)); + return false; + } + else + { + character.Speak(TextManager.Get("DialogReactorIsBroken"), identifier: "reactorisbroken", minDurationBetweenSimilar: 30.0f); + } + } if (TooMuchFuel()) { DropFuel(minCondition: 0.1f, maxCondition: 100); @@ -728,7 +783,7 @@ namespace Barotrauma.Items.Components case "set_fissionrate": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { - targetFissionRate = newFissionRate; + targetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f); if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; @@ -738,7 +793,7 @@ namespace Barotrauma.Items.Components case "set_turbineoutput": if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { - targetTurbineOutput = newTurbineOutput; + targetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f); if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 8287f8a42..7e9a9fc27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -57,6 +57,11 @@ namespace Barotrauma.Items.Components private Submarine controlledSub; + // AI interfacing + public Vector2 AITacticalTarget { get; set; } + public float AIRamTimer { get; set; } + bool navigateTactically; // this will be removed after rewriting steering to use an enum + private bool showIceSpireWarning; private List connectedSubs = new List(); @@ -305,7 +310,13 @@ namespace Barotrauma.Items.Components userSkill = user.GetSkillLevel("helm") / 100.0f; } - if (AutoPilot) + // override autopilot pathing while the AI rams, and go full speed ahead + if (AIRamTimer > 0f) + { + AIRamTimer -= deltaTime; + TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f); + } + else if (AutoPilot) { UpdateAutoPilot(deltaTime); float throttle = 1.0f; @@ -352,6 +363,14 @@ namespace Barotrauma.Items.Components float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f); item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out"); + + // if our tactical AI pilot has left, revert back to maintaining position + if (navigateTactically && (user == null || user.SelectedConstruction != item)) + { + navigateTactically = false; + AIRamTimer = 0f; + SetMaintainPosition(); + } } private void IncreaseSkillLevel(Character user, float deltaTime) @@ -580,13 +599,18 @@ namespace Barotrauma.Items.Components } Vector2 target; - if (LevelEndSelected) + + if (navigateTactically) { - target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition); + target = ConvertUnits.ToSimUnits(AITacticalTarget); + } + else if (LevelEndSelected) + { + target = ConvertUnits.ToSimUnits(Level.Loaded.EndExitPosition); } else { - target = ConvertUnits.ToSimUnits(Level.Loaded.StartPosition); + target = ConvertUnits.ToSimUnits(Level.Loaded.StartExitPosition); } steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(controlledSub == null ? item.WorldPosition : controlledSub.WorldPosition), target, errorMsgStr: "(Autopilot, target: " + target + ")"); } @@ -597,6 +621,7 @@ namespace Barotrauma.Items.Components MaintainPos = false; posToMaintain = null; LevelEndSelected = false; + navigateTactically = false; if (!LevelStartSelected) { LevelStartSelected = true; @@ -610,6 +635,7 @@ namespace Barotrauma.Items.Components MaintainPos = false; posToMaintain = null; LevelStartSelected = false; + navigateTactically = false; if (!LevelEndSelected) { LevelEndSelected = true; @@ -617,6 +643,36 @@ namespace Barotrauma.Items.Components } } + private void SetDestinationTactical() + { + AutoPilot = true; + MaintainPos = false; + posToMaintain = null; + LevelStartSelected = false; + LevelEndSelected = false; + if (!navigateTactically) + { + navigateTactically = true; + UpdatePath(); + } + } + + private void SetMaintainPosition() + { + if (!MaintainPos) + { + unsentChanges = true; + MaintainPos = true; + } + if (!posToMaintain.HasValue) + { + unsentChanges = true; + posToMaintain = controlledSub != null ? + controlledSub.WorldPosition : + item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition; + } + } + /// /// Get optimal velocity for moving towards a position /// @@ -640,6 +696,7 @@ namespace Barotrauma.Items.Components public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { + character.AIController.SteeringManager.Reset(); if (objective.Override) { if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) @@ -648,6 +705,20 @@ namespace Barotrauma.Items.Components } } user = character; + + if (Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(Item, character)) + { + if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f) + { + objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true)); + return false; + } + else + { + character.Speak(TextManager.Get("DialogNavTerminalIsBroken"), identifier: "navterminalisbroken", minDurationBetweenSimilar: 30.0f); + } + } + if (!AutoPilot) { unsentChanges = true; @@ -659,18 +730,7 @@ namespace Barotrauma.Items.Components case "maintainposition": if (objective.Override) { - if (!MaintainPos) - { - unsentChanges = true; - MaintainPos = true; - } - if (!posToMaintain.HasValue) - { - unsentChanges = true; - posToMaintain = controlledSub != null ? - controlledSub.WorldPosition : - item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition; - } + SetMaintainPosition(); } break; case "navigateback": @@ -681,7 +741,7 @@ namespace Barotrauma.Items.Components } if (objective.Override) { - if (MaintainPos || LevelEndSelected || !LevelStartSelected) + if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically) { unsentChanges = true; } @@ -696,13 +756,28 @@ namespace Barotrauma.Items.Components } if (objective.Override) { - if (MaintainPos || !LevelEndSelected || LevelStartSelected) + if (MaintainPos || !LevelEndSelected || LevelStartSelected || navigateTactically) { unsentChanges = true; } SetDestinationLevelEnd(); } break; + case "navigatetactical": + if (Level.IsLoadedOutpost) { break; } + if (DockingSources.Any(d => d.Docked)) + { + item.SendSignal("1", "toggle_docking"); + } + if (objective.Override) + { + if (MaintainPos || LevelEndSelected || LevelStartSelected || !navigateTactically) + { + unsentChanges = true; + } + SetDestinationTactical(); + } + break; } sonar?.AIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index 35de12b51..20cf5e402 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -137,16 +137,9 @@ namespace Barotrauma.Items.Components return true; } - if (HasAnyFinishedGrowing()) - { - Msg = MsgHarvest; - ParseMsg(); - return true; - } - - Msg = string.Empty; + Msg = MsgHarvest; ParseMsg(); - return false; + return true; } public override bool Pick(Character character) @@ -199,12 +192,13 @@ namespace Barotrauma.Items.Components { Debug.Assert(container != null, "Tried to harvest a planter without an item container."); + bool anyDecayed = GrowableSeeds.Any(s => s is { } seed && (seed.Decayed || seed.FullyGrown)); for (var i = 0; i < GrowableSeeds.Length; i++) { Growable? seed = GrowableSeeds[i]; if (seed == null) { continue; } - if (seed.Decayed || seed.FullyGrown) + if (!anyDecayed || seed.Decayed || seed.FullyGrown) { container?.Inventory.RemoveItem(seed.Item); Entity.Spawner?.AddToRemoveQueue(seed.Item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 84302e52a..1828f7cee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -359,7 +359,7 @@ namespace Barotrauma.Items.Components { //other junction boxes don't need to receive the signal in the pass-through signal connections //because we relay it straight to the connected items without going through the whole chain of junction boxes - if (ic is PowerTransfer && !(ic is RelayComponent) && connection.Name.Contains("signal")) { continue; } + if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } ic.ReceiveSignal(signal, recipient); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index a23a6ba9e..7e5cd03e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -20,7 +20,6 @@ namespace Barotrauma.Items.Components public Vector2 Point; public Vector2 Normal; public float Fraction; - public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction) { Fixture = fixture; @@ -45,6 +44,8 @@ namespace Barotrauma.Items.Components private readonly Queue impactQueue = new Queue(); + private bool removePending; + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -71,6 +72,8 @@ namespace Barotrauma.Items.Components } } + public Character Attacker { get; set; } + public IEnumerable Hits { get { return hits; } @@ -81,7 +84,11 @@ namespace Barotrauma.Items.Components [Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } + [Serialize(0.0f, false, description: "The random percentage modifier used to add variance to the launch impulse.")] + public float ImpulseSpread { get; set; } + [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] + public float LaunchRotation { get { return MathHelper.ToDegrees(LaunchRotationRadians); } @@ -168,6 +175,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + public bool StaticSpread + { + get; + set; + } + public Body StickTarget { get; @@ -189,7 +203,9 @@ namespace Barotrauma.Items.Components if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", Projectile"); } + InitProjSpecific(element); } + partial void InitProjSpecific(XElement element); public override void OnItemLoaded() { @@ -264,11 +280,26 @@ namespace Barotrauma.Items.Components for (int i = 0; i < HitScanCount; i++) { - float launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + float launchAngle; + + if (StaticSpread) + { + float staticSpread = Spread / (HitScanCount - 1); + // because the position of the item changes as hitscan are fired, we will set an + // initial offset on the first hitscan and then increase the item's angle by a set amount as hitscan are fired + float offset = i == 0 ? -staticSpread * (HitScanCount -1) : 0f; + launchAngle = item.body.Rotation + MathHelper.ToRadians(staticSpread + offset); + } + else + { + launchAngle = item.body.Rotation + MathHelper.ToRadians(Spread * Rand.Range(-0.5f, 0.5f)); + } + Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle)); if (Hitscan) { Vector2 prevSimpos = item.SimPosition; + item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); DoHitscan(launchDir); if (i < HitScanCount - 1) { @@ -277,7 +308,9 @@ namespace Barotrauma.Items.Components } else { - DoLaunch(launchDir * LaunchImpulse * item.body.Mass); + item.body.SetTransform(item.body.SimPosition, launchAngle); + float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); + DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); } } User = character; @@ -331,7 +364,14 @@ namespace Barotrauma.Items.Components IsActive = true; Vector2 rayStart = simPositon; - Vector2 rayEnd = simPositon + dir * 500.0f; + Vector2 rayEnd = rayStart + dir * 500.0f; + + Vector2 rayStartWorld = item.WorldPosition; + float worldDist = 1000.0f; +#if CLIENT + worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth; +#endif + Vector2 rayEndWorld = rayStartWorld + dir * worldDist; List hits = new List(); @@ -375,6 +415,7 @@ namespace Barotrauma.Items.Components item.SetTransform(h.Point, rotation); if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { + LaunchProjSpecific(rayStartWorld, item.WorldPosition); hitSomething = true; break; } @@ -383,6 +424,8 @@ namespace Barotrauma.Items.Components //the raycast didn't hit anything -> the projectile flew somewhere outside the level and is permanently lost if (!hitSomething) { + item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation); + LaunchProjSpecific(rayStartWorld, rayEndWorld); if (Entity.Spawner == null) { item.Remove(); @@ -413,8 +456,15 @@ namespace Barotrauma.Items.Components var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f); GameMain.World.QueryAABB((fixture) => { - //ignore sensors and items - if (fixture?.Body == null || fixture.IsSensor) { return true; } + if (fixture?.Body.UserData is LevelObject levelObj) + { + if (!levelObj.Prefab.TakeLevelWallDamage) { return true; } + } + else if (fixture?.Body == null || fixture.IsSensor) + { + //ignore sensors and items + return true; + } if (fixture.Body.UserData is VineTile) { 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") { return true; } @@ -422,6 +472,7 @@ namespace Barotrauma.Items.Components //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub if (submarine != null) { + if (fixture.Body.UserData is VoronoiCell) { return true; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; } } @@ -442,7 +493,15 @@ namespace Barotrauma.Items.Components GameMain.World.RayCast((fixture, point, normal, fraction) => { //ignore sensors and items - if (fixture?.Body == null || fixture.IsSensor) { return -1; } + if (fixture?.Body.UserData is LevelObject levelObj) + { + if (!levelObj.Prefab.TakeLevelWallDamage) { return -1; } + } + else if (fixture?.Body == null || fixture.IsSensor) + { + //ignore sensors and items + return -1; + } if (fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } @@ -456,21 +515,40 @@ namespace Barotrauma.Items.Components //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub if (submarine != null) { + if (fixture.Body.UserData is VoronoiCell) { return -1; } if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } } //ignore level cells if the item and the point of impact are inside a sub - if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null) + if (fixture.Body.UserData is VoronoiCell) { - if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null) + if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null && this.item.Submarine != null) { return -1; } } + if (hits.Count > 50) + { + float furthestHit = 0.0f; + int furthestHitIndex = -1; + for (int i = 0; i < hits.Count; i++) + { + if (hits[i].Fraction > furthestHit) + { + furthestHitIndex = i; + furthestHit = hits[i].Fraction; + } + } + if (furthestHitIndex > -1) + { + hits.RemoveAt(furthestHitIndex); + } + } + hits.Add(new HitscanResult(fixture, point, normal, fraction)); - return hits.Count < 25 ? 1 : 0; + return 1; }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel); return hits; @@ -488,14 +566,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity); } + if (!removePending) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + } + if (item.body != null && item.body.FarseerBody.IsBullet) { if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold) @@ -573,11 +654,11 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { - //severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive) if (limb.IsSevered) { - target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass); - return true; + //push the severed limb around a bit, but let the projectile pass through it + limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition); + return false; } } else if (target.Body.UserData is Item item) @@ -594,6 +675,10 @@ namespace Barotrauma.Items.Components hits.Add(target.Body); impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity)); IsActive = true; + if (RemoveOnHit) + { + item.body.FarseerBody.ResetDynamics(); + } if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { Deactivate(); @@ -605,6 +690,8 @@ namespace Barotrauma.Items.Components } } + readonly List targets = new List(); + private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { if (User != null && User.Removed) { User = null; } @@ -615,6 +702,9 @@ namespace Barotrauma.Items.Components return false; } + float projectileNewSpeed = 0.5f; + float projectileDeflectedNewSpeed = 0.1f; + AttackResult attackResult = new AttackResult(); Character character = null; if (target.Body.UserData is Submarine submarine) @@ -626,12 +716,16 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { - //severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive) - if (limb.IsSevered) { return true; } - if (limb.character == null || limb.character.Removed) { return false; } + // when hitting limbs with piercing ammo, don't lose as much speed + if (MaxTargetsToHit > 1) + { + projectileNewSpeed = 1f; + projectileDeflectedNewSpeed = 0.8f; + } + if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; } limb.character.LastDamageSource = item; - if (Attack != null) { attackResult = Attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); } + 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 is Item targetItem) @@ -639,18 +733,18 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return false; } if (Attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0) { - attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); } } else if (target.Body.UserData is IDamageable damageable) { - if (Attack != null) { attackResult = Attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); } + if (Attack != null) { attackResult = Attack.DoDamage(User ?? Attacker, damageable, item.WorldPosition, 1.0f); } } - else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f) + else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.LevelWallDamage) > 0.0f) { if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall) { - attackResult = Attack.DoDamage(User, destructibleWall, item.WorldPosition, 1.0f); + attackResult = Attack.DoDamage(User ?? Attacker, destructibleWall, item.WorldPosition, 1.0f); } } @@ -690,8 +784,8 @@ namespace Barotrauma.Items.Components if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - effect.GetNearbyTargets(targetLimb.WorldPosition, targets); + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets)); effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); } @@ -731,7 +825,7 @@ namespace Barotrauma.Items.Components if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles)) { - item.body.LinearVelocity *= 0.1f; + item.body.LinearVelocity *= projectileDeflectedNewSpeed; } else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && target.Body.Mass > item.body.Mass * 0.5f && @@ -761,13 +855,13 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif - item.body.LinearVelocity *= 0.5f; + item.body.LinearVelocity *= projectileNewSpeed; return Hitscan; } else { - item.body.LinearVelocity *= 0.5f; + item.body.LinearVelocity *= projectileNewSpeed; } var containedItems = item.OwnInventory?.AllItems; @@ -784,15 +878,10 @@ namespace Barotrauma.Items.Components if (RemoveOnHit) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) - { - //clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it - item.HiddenInGame = Hitscan; - } - else - { - Entity.Spawner?.AddToRemoveQueue(item); - } + removePending = true; + item.HiddenInGame = true; + item.body.FarseerBody.Enabled = false; + Entity.Spawner?.AddToRemoveQueue(item); } return true; @@ -876,5 +965,6 @@ namespace Barotrauma.Items.Components } } + partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 87db1cef9..1f923cc23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -10,7 +10,8 @@ namespace Barotrauma.Items.Components { partial class Rope : ItemComponent, IServerSerializable { - private Item source, target; + private ISpatialEntity source; + private Item target; private float snapTimer; private const float SnapAnimDuration = 1.0f; @@ -85,7 +86,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - public void Attach(Item source, Item target) + public void Attach(ISpatialEntity source, Item target) { System.Diagnostics.Debug.Assert(source != null); System.Diagnostics.Debug.Assert(target != null); @@ -97,7 +98,8 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (source == null || source.Removed || target == null || target.Removed) + if (source == null || target == null || target.Removed || + (source is Entity sourceEntity && sourceEntity.Removed)) { IsActive = false; return; @@ -195,19 +197,13 @@ namespace Barotrauma.Items.Components if (Math.Abs(SourcePullForce) > 0.001f) { var sourceBody = GetBodyToPull(source); - if (sourceBody != null) - { - sourceBody.ApplyForce(forceDir * SourcePullForce); - } + sourceBody?.ApplyForce(forceDir * SourcePullForce); } if (Math.Abs(TargetPullForce) > 0.001f) { var targetBody = GetBodyToPull(target); - if (targetBody != null) - { - targetBody.ApplyForce(-forceDir * TargetPullForce); - } + targetBody?.ApplyForce(-forceDir * TargetPullForce); } } @@ -224,32 +220,40 @@ namespace Barotrauma.Items.Components } } - private PhysicsBody GetBodyToPull(Item target) + private PhysicsBody GetBodyToPull(ISpatialEntity target) { - if (target.ParentInventory is CharacterInventory characterInventory && - characterInventory.Owner is Character ownerCharacter) + if (target is Item targetItem) { - if (ownerCharacter.Removed) { return null; } - return ownerCharacter.AnimController.Collider; + if (targetItem.ParentInventory is CharacterInventory characterInventory && + characterInventory.Owner is Character ownerCharacter) + { + if (ownerCharacter.Removed) { return null; } + return ownerCharacter.AnimController.Collider; + } + var projectile = targetItem.GetComponent(); + if (projectile != null) + { + if (projectile.StickTarget?.UserData is Structure structure) + { + return structure.Submarine?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Submarine sub) + { + return sub?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Character character) + { + return character.AnimController.Collider; + } + return null; + } + if (targetItem.body != null) { return targetItem.body; } } - var projectile = target.GetComponent(); - if (projectile != null) + else if (target is Limb targetLimb) { - if (projectile.StickTarget?.UserData is Structure structure) - { - return structure.Submarine?.PhysicsBody; - } - else if (projectile.StickTarget?.UserData is Submarine sub) - { - return sub?.PhysicsBody; - } - else if (projectile.StickTarget?.UserData is Character character) - { - return character.AnimController.Collider; - } - return null; + return targetLimb.body; } - if (target.body != null) { return target.body; } + return null; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 89543f080..38c56e427 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -34,6 +34,11 @@ namespace Barotrauma.Items.Components set; } + public bool TemporarilyLocked + { + get { return Level.IsLoadedOutpost && item.GetComponent() != null; } + } + //connection panels can't be deactivated externally (by signals or status effects) public override bool IsActive { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index a97527ea6..bc7f71527 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -95,7 +95,7 @@ namespace Barotrauma.Items.Components } private string[] labels; - [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.")] + [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] public string Labels { get { return string.Join(",", labels); } @@ -111,7 +111,7 @@ namespace Barotrauma.Items.Components } private string[] signals; - [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.")] + [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] public string Signals { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index cab1434f4..a836fab59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -20,9 +20,10 @@ namespace Barotrauma.Items.Components private bool castShadows; private bool drawBehindSubs; - private double lastToggleSignalTime; + private string prevColorSignal; + public PhysicsBody ParentBody; private Turret turret; @@ -37,7 +38,7 @@ namespace Barotrauma.Items.Components range = MathHelper.Clamp(value, 0.0f, 4096.0f); #if CLIENT item.ResetCachedVisibleSize(); - if (light != null) { light.Range = range; } + if (Light != null) { Light.Range = range; } #endif } } @@ -53,7 +54,7 @@ namespace Barotrauma.Items.Components { castShadows = value; #if CLIENT - if (light != null) light.CastShadows = value; + if (Light != null) Light.CastShadows = value; #endif } } @@ -67,7 +68,7 @@ namespace Barotrauma.Items.Components { drawBehindSubs = value; #if CLIENT - if (light != null) light.IsBackground = drawBehindSubs; + if (Light != null) Light.IsBackground = drawBehindSubs; #endif } } @@ -93,7 +94,7 @@ namespace Barotrauma.Items.Components { flicker = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - if (light != null) { light.LightSourceParams.Flicker = flicker; } + if (Light != null) { Light.LightSourceParams.Flicker = flicker; } #endif } } @@ -106,7 +107,7 @@ namespace Barotrauma.Items.Components { flickerSpeed = value; #if CLIENT - if (light != null) { light.LightSourceParams.FlickerSpeed = flickerSpeed; } + if (Light != null) { Light.LightSourceParams.FlickerSpeed = flickerSpeed; } #endif } } @@ -119,7 +120,7 @@ namespace Barotrauma.Items.Components { pulseFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); #if CLIENT - if (light != null) { light.LightSourceParams.PulseFrequency = pulseFrequency; } + if (Light != null) { Light.LightSourceParams.PulseFrequency = pulseFrequency; } #endif } } @@ -132,7 +133,7 @@ namespace Barotrauma.Items.Components { pulseAmount = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - if (light != null) { light.LightSourceParams.PulseAmount = pulseAmount; } + if (Light != null) { Light.LightSourceParams.PulseAmount = pulseAmount; } #endif } } @@ -145,7 +146,7 @@ namespace Barotrauma.Items.Components { blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); #if CLIENT - if (light != null) { light.LightSourceParams.BlinkFrequency = blinkFrequency; } + if (Light != null) { Light.LightSourceParams.BlinkFrequency = blinkFrequency; } #endif } } @@ -158,7 +159,7 @@ namespace Barotrauma.Items.Components { lightColor = value; #if CLIENT - if (light != null) light.Color = IsActive ? lightColor : Color.Transparent; + if (Light != null) Light.Color = IsActive ? lightColor : Color.Transparent; #endif } } @@ -173,7 +174,7 @@ namespace Barotrauma.Items.Components public override void Move(Vector2 amount) { #if CLIENT - light.Position += amount; + Light.Position += amount; #endif } @@ -197,7 +198,7 @@ namespace Barotrauma.Items.Components : base(item, element) { #if CLIENT - light = new LightSource(element) + Light = new LightSource(element) { ParentSub = item.CurrentHull?.Submarine, Position = item.Position, @@ -206,11 +207,11 @@ namespace Barotrauma.Items.Components SpriteScale = Vector2.One * item.Scale, Range = range }; - light.LightSourceParams.Flicker = flicker; - light.LightSourceParams.FlickerSpeed = FlickerSpeed; - light.LightSourceParams.PulseAmount = pulseAmount; - light.LightSourceParams.PulseFrequency = pulseFrequency; - light.LightSourceParams.BlinkFrequency = blinkFrequency; + Light.LightSourceParams.Flicker = flicker; + Light.LightSourceParams.FlickerSpeed = FlickerSpeed; + Light.LightSourceParams.PulseAmount = pulseAmount; + Light.LightSourceParams.PulseFrequency = pulseFrequency; + Light.LightSourceParams.BlinkFrequency = blinkFrequency; #endif IsActive = IsOn; @@ -233,7 +234,7 @@ namespace Barotrauma.Items.Components UpdateOnActiveEffects(deltaTime); #if CLIENT - light.ParentSub = item.Submarine; + Light.ParentSub = item.Submarine; #endif if (item.Container != null) { @@ -243,23 +244,23 @@ namespace Barotrauma.Items.Components #if CLIENT if (ParentBody != null) { - light.Position = ParentBody.Position; + Light.Position = ParentBody.Position; } else if (turret != null) { - light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); + Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); } else { - light.Position = item.Position; + Light.Position = item.Position; } #endif PhysicsBody body = ParentBody ?? item.body; if (body != null) { #if CLIENT - light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; - light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; + Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; + Light.LightSpriteEffect = (body.Dir > 0.0f) ? SpriteEffects.None : SpriteEffects.FlipVertically; #endif if (!body.Enabled) { @@ -270,8 +271,8 @@ namespace Barotrauma.Items.Components else { #if CLIENT - light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); - light.LightSpriteEffect = item.SpriteEffects; + Light.Rotation = -Rotation - MathHelper.ToRadians(item.Rotation); + Light.LightSpriteEffect = item.SpriteEffects; #endif } @@ -326,7 +327,11 @@ namespace Barotrauma.Items.Components IsOn = signal.value != "0"; break; case "set_color": - LightColor = XMLExtensions.ParseColor(signal.value, false); + if (signal.value != prevColorSignal) + { + LightColor = XMLExtensions.ParseColor(signal.value, false); + prevColorSignal = signal.value; + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index f6ca3513c..0ead9540d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -8,7 +8,6 @@ namespace Barotrauma.Items.Components { partial class MotionSensor : ItemComponent { - private const float UpdateInterval = 0.1f; private float rangeX, rangeY; private Vector2 detectOffset; @@ -19,7 +18,8 @@ namespace Barotrauma.Items.Components { Any, Human, - Monster + Monster, + Wall } [Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -74,6 +74,13 @@ namespace Barotrauma.Items.Components } } + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] + public float UpdateInterval + { + get; + set; + } + private int maxOutputLength; [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength @@ -134,6 +141,9 @@ namespace Barotrauma.Items.Components { rangeX = rangeY = element.GetAttributeFloat("range", 0.0f); } + + //randomize update timer so all sensors aren't updated during the same frame + updateTimer = Rand.Range(0.0f, UpdateInterval); } public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) @@ -153,7 +163,7 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); } updateTimer -= deltaTime; - if (updateTimer > 0.0f) return; + if (updateTimer > 0.0f) { return; } MotionDetected = false; updateTimer = UpdateInterval; @@ -163,6 +173,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity) { MotionDetected = true; + return; } } @@ -171,42 +182,94 @@ namespace Barotrauma.Items.Components float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - foreach (Character c in Character.CharacterList) + if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null && + (Target == TargetType.Wall || Target == TargetType.Any)) { - if (IgnoreDead && c.IsDead) { continue; } - - //ignore characters that have spawned a second or less ago - //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground - if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - - switch (Target) + if (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity) { - case TargetType.Human: - if (!c.IsHuman) { continue; } - break; - case TargetType.Monster: - if (c.IsHuman || c.IsPet) { continue; } - break; + var cells = Level.Loaded.GetCells(item.WorldPosition, 1); + foreach (var cell in cells) + { + if (cell.IsPointInside(item.WorldPosition)) + { + MotionDetected = true; + return; + } + foreach (var edge in cell.Edges) + { + Vector2 e1 = edge.Point1 + cell.Translation; + Vector2 e2 = edge.Point2 + cell.Translation; + if (MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom))) + { + MotionDetected = true; + return; + } + } + } } - - //do a rough check based on the position of the character's collider first - //before the more accurate limb-based check - if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + foreach (Submarine sub in Submarine.Loaded) { - continue; - } + if (sub == item.Submarine) { continue; } - foreach (Limb limb in c.AnimController.Limbs) - { - if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } - if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) + Vector2 relativeVelocity = item.Submarine.Velocity - sub.Velocity; + if (Math.Abs(relativeVelocity.X) < MinimumVelocity && Math.Abs(relativeVelocity.Y) < MinimumVelocity) { continue; } + + Rectangle worldBorders = new Rectangle( + sub.Borders.X + (int)sub.WorldPosition.X, + sub.Borders.Y + (int)sub.WorldPosition.Y - sub.Borders.Height, + sub.Borders.Width, + sub.Borders.Height); + + if (worldBorders.Intersects(detectRect)) { MotionDetected = true; - break; + return; } } } + + if (Target != TargetType.Wall) + { + foreach (Character c in Character.CharacterList) + { + if (IgnoreDead && c.IsDead) { continue; } + + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + + switch (Target) + { + case TargetType.Human: + if (!c.IsHuman) { continue; } + break; + case TargetType.Monster: + if (c.IsHuman || c.IsPet) { continue; } + break; + } + + //do a rough check based on the position of the character's collider first + //before the more accurate limb-based check + if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + { + continue; + } + + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } + if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) + { + MotionDetected = true; + return; + } + } + } + } } public override void FlipX(bool relativeToSub) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs index c3c007b44..6680d82ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Signal.cs @@ -19,5 +19,22 @@ namespace Barotrauma.Items.Components this.power = power; this.strength = strength; } + + internal Signal WithStepsTaken(int stepsTaken) + { + Signal retVal = this; + retVal.stepsTaken = stepsTaken; + return retVal; + } + + public static bool operator==(Signal a, Signal b) => + a.value == b.value && + a.stepsTaken == b.stepsTaken && + a.sender == b.sender && + a.source == b.source && + MathUtils.NearlyEqual(a.power, b.power) && + MathUtils.NearlyEqual(a.strength, b.strength); + + public static bool operator!=(Signal a, Signal b) => !(a == b); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 8588d9bdd..7d3e3caad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -64,6 +64,7 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); + bool sendOutputImmediately = true; switch (Function) { case FunctionType.Sin: @@ -105,11 +106,13 @@ namespace Barotrauma.Items.Components { timeSinceReceived[0] = 0.0f; float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + sendOutputImmediately = false; } else if (connection.Name == "signal_in_y") { timeSinceReceived[1] = 0.0f; float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + sendOutputImmediately = false; } else { @@ -121,9 +124,11 @@ namespace Barotrauma.Items.Components default: throw new NotImplementedException($"Function {Function} has not been implemented."); } - - signal.value = value.ToString("G", CultureInfo.InvariantCulture); - item.SendSignal(signal, "signal_out"); + if (sendOutputImmediately) + { + signal.value = value.ToString("G", CultureInfo.InvariantCulture); + item.SendSignal(signal, "signal_out"); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 6ca44bef8..d64c4ea32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -58,7 +58,8 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)] + [Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index d0cce2fa9..01058e3e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Items.Components get { if (GameMain.NetworkMember?.ServerSettings != null && !GameMain.NetworkMember.ServerSettings.AllowRewiring) { return false; } - return locked || connections.Any(c => c != null && c.ConnectionPanel.Locked); + return locked || connections.Any(c => c != null && (c.ConnectionPanel.Locked || c.ConnectionPanel.TemporarilyLocked)); } set { locked = value; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 11740b981..7e93b1ab2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components partial class Turret : Powered, IDrawableComponent, IServerSerializable { private Sprite barrelSprite, railSprite; + private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>(); + private readonly List spinningBarrelSprites = new List(); private Vector2 barrelPos; private Vector2 transformedBarrelPos; @@ -35,6 +37,20 @@ namespace Barotrauma.Items.Components private int failedLaunchAttempts; + private float currentChargeTime; + private bool tryingToCharge; + + private enum ChargingState + { + Inactive, + WindingUp, + WindingDown, + } + + private ChargingState currentChargingState; + + private float currentBarrelSpin = 0f; + private readonly List activeProjectiles = new List(); public IEnumerable ActiveProjectiles => activeProjectiles; @@ -42,6 +58,12 @@ namespace Barotrauma.Items.Components private float resetUserTimer; + private float aiTargetingGraceTimer; + + private float aiFindTargetTimer; + private Character currentTarget; + const float aiFindTargetInterval = 5.0f; + public float Rotation { get { return rotation; } @@ -61,6 +83,12 @@ namespace Barotrauma.Items.Components } } + [Serialize("0,0", false, description: "The projectile launching location relative to transformed barrel position (in pixels).")] + public Vector2 FiringOffset + { + get; + set; + } public Vector2 TransformedBarrelPos { get @@ -198,6 +226,20 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(1.0f, false, description: "How fast the turret can rotate while firing (for charged weapons).")] + public float FiringRotationSpeedModifier + { + get; + private set; + } + + [Serialize(false, true, description: "Whether the turret should always charge-up fully to shoot.")] + public bool SingleChargedShot + { + get; + private set; + } + private float prevScale; float prevBaseRotation; [Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] @@ -225,6 +267,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + public float MaxChargeTime + { + get; + private set; + } + public Turret(Item item, XElement element) : base(item, element) { @@ -240,6 +289,16 @@ namespace Barotrauma.Items.Components case "railsprite": railSprite = new Sprite(subElement); break; + case "chargesprite": + chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero))); + break; + case "spinningbarrelsprite": + int spriteCount = subElement.GetAttributeInt("spriteamount", 1); + for (int i = 0; i < spriteCount; i++) + { + spinningBarrelSprites.Add(new Sprite(subElement)); + } + break; } } item.IsShootable = true; @@ -311,6 +370,35 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + float previousChargeTime = currentChargeTime; + + if (SingleChargedShot && reload > 0f) + { + // single charged shot guns will decharge after firing + // for cosmetic reasons, this is done by lerping in half the reload time + currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)); + } + else + { + float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime; + currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime); + } + tryingToCharge = false; + + if (currentChargeTime == 0f) + { + currentChargingState = ChargingState.Inactive; + } + else if (currentChargeTime < previousChargeTime) + { + currentChargingState = ChargingState.WindingDown; + } + else + { + // if we are charging up or at maxed charge, remain winding up + currentChargingState = ChargingState.WindingUp; + } + UpdateProjSpecific(deltaTime); if (MathUtils.NearlyEqual(minRotation, maxRotation)) @@ -333,6 +421,10 @@ namespace Barotrauma.Items.Components float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess); float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess); float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess); + if (MaxChargeTime > 0) + { + rotationSpeed *= MathHelper.Lerp(1f, FiringRotationSpeedModifier, MathUtils.EaseIn(currentChargeTime / MaxChargeTime)); + } // Do not increase the weapons skill when operating a turret in an outpost level if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost)) @@ -384,6 +476,15 @@ namespace Barotrauma.Items.Components angularVelocity *= -0.5f; } + if (aiTargetingGraceTimer > 0f) + { + aiTargetingGraceTimer -= deltaTime; + } + if (aiFindTargetTimer > 0.0f) + { + aiFindTargetTimer -= deltaTime; + } + UpdateLightComponent(); } @@ -403,10 +504,18 @@ namespace Barotrauma.Items.Components return TryLaunch(deltaTime, character); } + public bool HasPowerToShoot() + { + return GetAvailableBatteryPower() >= powerConsumption; + } + private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) { + tryingToCharge = true; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + if (currentChargeTime < MaxChargeTime) { return false; } + if (reload > 0.0f) { return false; } if (MaxActiveProjectiles >= 0) @@ -420,7 +529,7 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - if (GetAvailableBatteryPower() < powerConsumption) + if (!HasPowerToShoot()) { #if CLIENT if (!flashLowPower && character != null && character == Character.Controlled) @@ -434,13 +543,17 @@ namespace Barotrauma.Items.Components } Projectile launchedProjectile = null; + bool loaderBroken = false; for (int i = 0; i < ProjectileCount; i++) { - var projectiles = GetLoadedProjectiles(true); + var projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent(); - if (projectileContainer?.Item != item) { projectileContainer?.Item.Use(deltaTime, null); } + if (projectileContainer != null && projectileContainer.Item != item) + { + projectileContainer?.Item.Use(deltaTime, null); + } } else { @@ -449,11 +562,16 @@ namespace Barotrauma.Items.Components //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) if (!(e is Item linkedItem)) { continue; } + if (linkedItem.Condition <= 0.0f) + { + loaderBroken = true; + continue; + } ItemContainer projectileContainer = linkedItem.GetComponent(); if (projectileContainer != null) { linkedItem.Use(deltaTime, null); - projectiles = GetLoadedProjectiles(true); + projectiles = GetLoadedProjectiles(); if (projectiles.Any()) { break; } } } @@ -465,9 +583,16 @@ namespace Barotrauma.Items.Components // -> attempt to launch the gun multiple times before showing the "no ammo" flash failedLaunchAttempts++; #if CLIENT - if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20) + if (!flashNoAmmo && !flashLoaderBroken && character != null && character == Character.Controlled && failedLaunchAttempts > 20) { - flashNoAmmo = true; + if (loaderBroken) + { + flashLoaderBroken = true; + } + else + { + flashNoAmmo = true; + } failedLaunchAttempts = 0; SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } @@ -475,7 +600,6 @@ namespace Barotrauma.Items.Components return false; } failedLaunchAttempts = 0; - launchedProjectile = projectiles.FirstOrDefault(); if (!ignorePower) { var batteries = item.GetConnectedComponents(); @@ -496,6 +620,7 @@ namespace Barotrauma.Items.Components } } + launchedProjectile = projectiles.FirstOrDefault(); if (launchedProjectile?.Item.Container != null) { var repairable = launchedProjectile?.Item.Container.GetComponent(); @@ -507,7 +632,17 @@ namespace Barotrauma.Items.Components if (launchedProjectile != null || LaunchWithoutProjectile) { - Launch(launchedProjectile?.Item, character); + if (projectiles.Any()) + { + foreach (Projectile projectile in projectiles) + { + Launch(projectile.Item, character); + } + } + else + { + Launch(null, character); + } if (item.AiTarget != null) { item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange; @@ -550,10 +685,10 @@ namespace Barotrauma.Items.Components projectile.body.ResetDynamics(); projectile.body.Enabled = true; } - + float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); projectile.SetTransform( - ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), + ConvertUnits.ToSimUnits(GetRelativeFiringPosition()), -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -561,7 +696,8 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { - projectileComponent.Use((float)Timing.Step); + projectileComponent.Attacker = user; + projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -780,20 +916,19 @@ namespace Barotrauma.Items.Components TryLaunch(deltaTime, ignorePower: true); } - private bool outOfAmmo; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 10.0f); + character.Speak(TextManager.Get("DialogTurretTargetDead"), identifier: "killedtarget" + previousTarget.ID, minDurationBetweenSimilar: 10.0f); character.AIController.SelectTarget(null); } - if (GetAvailableBatteryPower() < powerConsumption) + bool canShoot = true; + if (!HasPowerToShoot()) { var batteries = item.GetConnectedComponents(); - float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) @@ -804,15 +939,31 @@ namespace Barotrauma.Items.Components batteryToLoad = battery; lowestCharge = battery.Charge; } + if (battery.Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(battery.Item, character)) + { + if (battery.Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f) + { + objective.AddSubObjective(new AIObjectiveRepairItem(character, battery.Item, objective.objectiveManager, isPriority: true)); + return false; + } + else + { + character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), identifier: "supercapacitorisbroken", minDurationBetweenSimilar: 30.0f); + canShoot = false; + } + } } - - if (batteryToLoad == null) return true; - + if (batteryToLoad == null) { return true; } if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f) { objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false)); return false; } + if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0) + { + character.Speak(TextManager.Get("DialogTurretHasNoPower"), identifier: "turrethasnopower", minDurationBetweenSimilar: 30.0f); + canShoot = false; + } } int usableProjectileCount = 0; @@ -849,7 +1000,7 @@ namespace Barotrauma.Items.Components { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "cannotloadturret", minDurationBetweenSimilar: 30.0f); } return true; } @@ -859,7 +1010,7 @@ namespace Barotrauma.Items.Components loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), identifier: "loadturret", minDurationBetweenSimilar: 30.0f); } loadItemsObjective.Abandoned += CheckRemainingAmmo; loadItemsObjective.Completed += CheckRemainingAmmo; @@ -868,15 +1019,16 @@ namespace Barotrauma.Items.Components void CheckRemainingAmmo() { if (!character.IsOnPlayerTeam) { return; } - string ammoType = container.Item.HasTag("railgunammosource") ? "railgunammo" : container.Item.HasTag("coilgunammosource") ? "coilgunammo" : "turretammo"; + if (character.Submarine != Submarine.MainSub) { return; } + string ammoType = container.ContainableItems.First().Identifiers.FirstOrDefault() ?? "ammobox"; int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { - character.Speak(TextManager.Get($"DialogOutOf{ammoType}"), null, 0.0f, "outofammo", 30.0f); + character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f); } else if (remainingAmmo < 3) { - character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), null, 0.0f, "outofammo", 30.0f); + character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), identifier: "outofammo", minDurationBetweenSimilar: 30.0f); } } } @@ -891,24 +1043,48 @@ namespace Barotrauma.Items.Components Vector2? targetPos = null; float maxDistance = 10000; float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; - float closestDistance = maxDistance * maxDistance; - foreach (Character enemy in Character.CharacterList) + // use full range only if we're actively firing + if (aiTargetingGraceTimer <= 0f) { - // Ignore dead, friendly, and those that are inside the same sub - if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - // Don't aim monsters that are inside a submarine. - if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } - if (HumanAIController.IsFriendly(character, enemy)) { continue; } - float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); - if (dist > closestDistance) { continue; } - if (dist < shootDistance * shootDistance) + shootDistance *= 0.75f; + } + + float closestDistance = maxDistance * maxDistance; + + if (currentTarget != null) + { + if (currentTarget.Removed || currentTarget.IsDead) { - // Only check the angle to targets that are close enough to be shot at - // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. - if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + currentTarget = null; } - closestEnemy = enemy; - closestDistance = dist; + } + + if (aiFindTargetTimer <= 0.0f || currentTarget == null) + { + foreach (Character enemy in Character.CharacterList) + { + // Ignore dead, friendly, and those that are inside the same sub + if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } + // Don't aim monsters that are inside a submarine. + if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; } + if (HumanAIController.IsFriendly(character, enemy)) { continue; } + float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); + if (dist > closestDistance) { continue; } + if (dist < shootDistance * shootDistance) + { + // Only check the angle to targets that are close enough to be shot at + // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. + if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + } + closestEnemy = enemy; + closestDistance = dist; + } + currentTarget = closestEnemy; + aiFindTargetTimer = aiFindTargetInterval; + } + else + { + closestEnemy = currentTarget; } if (closestEnemy != null) @@ -938,6 +1114,7 @@ namespace Barotrauma.Items.Components else if (item.Submarine != null && Level.Loaded != null) { // Check ice spires + shootDistance = AIRange * item.OffsetOnSelectedMultiplier; closestDistance = shootDistance; foreach (var wall in Level.Loaded.ExtraWalls) { @@ -990,6 +1167,8 @@ namespace Barotrauma.Items.Components } if (targetPos == null) { return false; } + // Force the highest priority so that we don't change the objective while targeting enemies. + objective.ForceHighestPriority = true; if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget) { @@ -1032,7 +1211,14 @@ namespace Barotrauma.Items.Components float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition); float turretAngle = -rotation; - if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; } + float maxAngleError = 0.15f; + if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f) + { + //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target) + maxAngleError *= 2.0f; + } + + if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; } Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); @@ -1084,14 +1270,28 @@ namespace Barotrauma.Items.Components return false; } } - if (character.IsOnPlayerTeam) + if (canShoot) { - character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + } + character.SetInput(InputType.Shoot, true, true); } - character.SetInput(InputType.Shoot, true, true); + aiTargetingGraceTimer = 5f; return false; } + private Vector2 GetRelativeFiringPosition(bool useOffset = true) + { + Vector2 transformedFiringOffset = Vector2.Zero; + if (useOffset) + { + transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-FiringOffset.Y, -FiringOffset.X) * item.Scale, -rotation); + } + return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y); + } + private bool CheckTurretAngle(float angle) { float midRotation = (minRotation + maxRotation) / 2.0f; @@ -1116,22 +1316,28 @@ namespace Barotrauma.Items.Components #endif } - private List GetLoadedProjectiles(bool returnFirst = false) + private List GetLoadedProjectiles() { List projectiles = new List(); - //check the item itself first - CheckProjectileContainer(item, projectiles, returnFirst); + // check the item itself first + CheckProjectileContainer(item, projectiles, out bool _); foreach (MapEntity e in item.linkedTo) { - if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, returnFirst); } - if (returnFirst && projectiles.Any()) { return projectiles; } + if (e is Item projectileContainer) + { + CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching); + if (projectiles.Any() || stopSearching) { return projectiles; } + } } return projectiles; } - private void CheckProjectileContainer(Item projectileContainer, List projectiles, bool returnFirst) + private void CheckProjectileContainer(Item projectileContainer, List projectiles, out bool stopSearching) { + stopSearching = false; + if (projectileContainer.Condition <= 0.0f) { return; } + var containedItems = projectileContainer.ContainedItems; if (containedItems == null) { return; } @@ -1141,7 +1347,7 @@ namespace Barotrauma.Items.Components if (projectileComponent != null && projectileComponent.Item.body != null) { projectiles.Add(projectileComponent); - if (returnFirst) { return; } + return; } else { @@ -1152,9 +1358,15 @@ namespace Barotrauma.Items.Components if (projectileComponent != null && projectileComponent.Item.body != null) { projectiles.Add(projectileComponent); - if (returnFirst) { return; } } } + // in the case that we found a container that still has condition/ammo left, + // return and inform GetLoadedProjectiles to stop searching past this point (even if no projectiles were not found) + if (containedItem.Condition > 0.0f || projectiles.Any()) + { + stopSearching = true; + return; + } } } } @@ -1267,7 +1479,7 @@ namespace Barotrauma.Items.Components { if (extraData.Length > 2) { - msg.Write(!(extraData[2] is Item item) || item.Removed ? ushort.MaxValue : item.ID); + msg.Write(!(extraData[2] is Item item) ? ushort.MaxValue : item.ID); msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index d9a1ba7fd..b541c4812 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -51,6 +51,9 @@ namespace Barotrauma public bool InheritTextureScale { get; private set; } public bool InheritOrigin { get; private set; } public bool InheritSourceRect { get; private set; } + + public float Scale { get; private set; } + public LimbType DepthLimb { get; private set; } private Wearable _wearableComponent; public Wearable WearableComponent @@ -161,10 +164,7 @@ namespace Barotrauma if (IsInitialized) { return; } _gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None; ParsePath(false); - if (Sprite != null) - { - Sprite.Remove(); - } + Sprite?.Remove(); Sprite = new Sprite(SourceElement, file: SpritePath); Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); @@ -175,6 +175,7 @@ namespace Barotrauma InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); + Scale = SourceElement.GetAttributeFloat("scale", 1.0f); var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1)); if (index.X > -1 && index.Y > -1) { @@ -201,7 +202,7 @@ namespace Barotrauma namespace Barotrauma.Items.Components { - class Wearable : Pickable, IServerSerializable + partial class Wearable : Pickable, IServerSerializable { private readonly XElement[] wearableElements; private readonly WearableSprite[] wearableSprites; @@ -209,6 +210,7 @@ namespace Barotrauma.Items.Components private readonly Limb[] limb; private readonly List damageModifiers; + public readonly Dictionary SkillModifiers; public IEnumerable DamageModifiers { @@ -264,7 +266,8 @@ namespace Barotrauma.Items.Components this.item = item; damageModifiers = new List(); - + SkillModifiers = new Dictionary(); + int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server); @@ -307,18 +310,35 @@ namespace Barotrauma.Items.Components case "damagemodifier": damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable")); break; + case "skillmodifier": + string skillIdentifier = subElement.GetAttributeString("skillidentifier", string.Empty); + float skillValue = subElement.GetAttributeFloat("skillvalue", 0f); + if (SkillModifiers.ContainsKey(skillIdentifier)) + { + SkillModifiers[skillIdentifier] += skillValue; + } + else + { + SkillModifiers.TryAdd(skillIdentifier, skillValue); + } + break; } } } public override void Equip(Character character) { + foreach (var allowedSlot in allowedSlots) + { + if (allowedSlot != InvSlotType.Any && !character.Inventory.IsInLimbSlot(item, allowedSlot)) { return; } + } + picker = character; for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker.Info?.Gender ?? Gender.None); } - if (picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) + if (picker.Info != null && picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) { // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. wearableSprite.Gender = picker.Info.Gender; @@ -380,6 +400,7 @@ namespace Barotrauma.Items.Components { if (character == null || character.Removed) { return; } if (picker == null) { return; } + for (int i = 0; i < wearableSprites.Length; i++) { Limb equipLimb = character.AnimController.GetLimb(limbType[i]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 999f1e80a..7aa2ccb0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -670,7 +670,11 @@ namespace Barotrauma if (!swapSuccessful && existingItems.Count == 1 && existingItems[0].AllowDroppingOnSwapWith(item)) { - existingItems[0].Drop(user, createNetworkEvent); + if (!(existingItems[0].Container?.ParentInventory is CharacterInventory characterInv) || + !characterInv.TryPutItem(existingItems[0], user, new List() { InvSlotType.Any })) + { + existingItems[0].Drop(user, createNetworkEvent); + } swapSuccessful = stackedItems.Distinct().Any(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); #if CLIENT if (swapSuccessful) @@ -882,7 +886,7 @@ namespace Barotrauma } } } - + /// /// Deletes all items inside the inventory (and also recursively all items inside the items) /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index b6b8b989d..d93664572 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -6,7 +6,6 @@ using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -19,7 +18,6 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable { public static List ItemList = new List(); @@ -41,7 +39,24 @@ namespace Barotrauma ParentRuin = currentHull?.ParentRuin; } } - + + + private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None; + public CampaignMode.InteractionType CampaignInteractionType + { + get { return campaignInteractionType; } + set + { + if (campaignInteractionType != value) + { + campaignInteractionType = value; + AssignCampaignInteractionTypeProjSpecific(campaignInteractionType); + } + } + } + + partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType); + public bool Visible = true; #if CLIENT @@ -109,7 +124,8 @@ namespace Barotrauma foreach (ItemComponent component in components) { if (!component.AllowInGameEditing) { continue; } - if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType().Any())) + if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType().Any()) + || component.SerializableProperties.Values.Any(p => p.Attributes.OfType().Any(a => a.IsEditable(this)))) { hasInGameEditableProperties = true; break; @@ -181,6 +197,20 @@ namespace Barotrauma set; } + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, true, alwaysUseInstanceValues: true)] + public bool AllowSwapping + { + get; + set; + } + + [Serialize(false, true)] + public bool PurchasedNewSwap + { + get; + set; + } + /// /// Checks both and /// @@ -609,12 +639,12 @@ namespace Barotrauma } //which type of inventory slots (head, torso, any, etc) the item can be placed in + private readonly HashSet allowedSlots = new HashSet(); public IEnumerable AllowedSlots { get { - Pickable p = GetComponent(); - return (p == null) ? InvSlotType.Any.ToEnumerable() : p.AllowedSlots; + return allowedSlots; } } @@ -661,8 +691,35 @@ namespace Barotrauma get { return Prefab.Linkable; } } + /// + /// Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has been changed) + /// + public float PositionX + { + get { return Position.X; } + private set + { + Move(new Vector2((value - Position.X) * Scale, 0.0f)); + } + } + /// + /// Can be used to move the item from XML (e.g. to correct the positions of items whose sprite origin has been changed) + /// + public float PositionY + { + get { return Position.Y; } + private set + { + Move(new Vector2(0.0f, (value - Position.Y) * Scale)); + } + } + public BallastFloraBranch Infector { get; set; } + public ItemPrefab PendingItemSwap { get; set; } + + public readonly HashSet AvailableSwaps = new HashSet(); + public override string ToString() { #if CLIENT @@ -678,7 +735,7 @@ namespace Barotrauma get { return allPropertyObjects; } } - public bool IgnoreByAI => OrderedToBeIgnored || HasTag("ignorebyai"); + public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) @@ -765,6 +822,7 @@ namespace Barotrauma case "deconstruct": case "brokensprite": case "decorativesprite": + case "upgradepreviewsprite": case "price": case "levelcommonness": case "suitabletreatment": @@ -779,6 +837,7 @@ namespace Barotrauma case "minimapicon": case "infectedsprite": case "damagedinfectedsprite": + case "swappableitem": break; case "staticbody": StaticBodyConfig = subElement; @@ -805,7 +864,15 @@ namespace Barotrauma hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length]; foreach (ItemComponent ic in components) { - if (ic.statusEffectLists == null) continue; + if (ic is Pickable pickable) + { + foreach (var allowedSlot in pickable.AllowedSlots) + { + allowedSlots.Add(allowedSlot); + } + } + + if (ic.statusEffectLists == null) { continue; } if (statusEffectLists == null) { @@ -1034,23 +1101,26 @@ namespace Barotrauma { return (T)component; } - + if (typeof(T) == typeof(ItemComponent)) + { + return (T)components.FirstOrDefault(); + } return default; } public IEnumerable GetComponents() { + if (typeof(T) == typeof(ItemComponent)) + { + return components.Cast(); + } if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty(); } - return components.Where(c => c is T).Cast(); } public void RemoveContained(Item contained) { - if (ownInventory != null) - { - ownInventory.RemoveItem(contained); - } + ownInventory?.RemoveItem(contained); contained.Container = null; } @@ -1240,16 +1310,16 @@ namespace Barotrauma /// /// Should this item or any of its containers be ignored by the AI? /// - public bool IsThisOrAnyContainerIgnoredByAI() + public bool IsThisOrAnyContainerIgnoredByAI(Character character) { - if (IgnoreByAI) { return true; } + if (IgnoreByAI(character)) { return true; } if (Container == null) { return false; } - if (Container.IgnoreByAI) { return true; } + if (Container.IgnoreByAI(character)) { return true; } var container = Container; while (container.Container != null) { container = container.Container; - if (container.IgnoreByAI) { return true; } + if (container.IgnoreByAI(character)) { return true; } } return false; } @@ -1382,8 +1452,11 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems)) { - effect.GetNearbyTargets(WorldPosition, targets); - if (targets.Count > 0) { hasTargets = true; } + targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets)); + if (targets.Count > 0) + { + hasTargets = true; + } } if (effect.HasTargetType(StatusEffect.TargetType.UseTarget) && useTarget is ISerializableEntity serializableTarget) @@ -1932,26 +2005,36 @@ namespace Barotrauma SendSignal(signal, connection); } + private readonly HashSet<(Signal Signal, Connection Connection)> delayedSignals = new HashSet<(Signal Signal, Connection Connection)>(); + public void SendSignal(Signal signal, Connection connection) { LastSentSignalRecipients.Clear(); if (connections == null || connection == null) { return; } signal.stepsTaken++; - + + //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops + if (signal.stepsTaken > 5 && signal.source != null) + { + if (signal.source.LastSentSignalRecipients.AtLeast(3, recipient => recipient == connection)) + { + return; + } + } + + //use a coroutine to prevent infinite loops by creating a one + //frame delay if the "signal chain" gets too long if (signal.stepsTaken > 10) { - //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops - if (signal.source != null) + //if there's an equal signal waiting to be sent + //to the same connection, don't add a new one + signal.stepsTaken = 0; + if (!delayedSignals.Contains((signal, connection))) { - if (signal.source.LastSentSignalRecipients.Count(recipient => recipient == connection) > 2) - { - return; - } + delayedSignals.Add((signal, connection)); + CoroutineManager.StartCoroutine(DelaySignal(signal, connection)); } - //use a coroutine to prevent infinite loops by creating a one - //frame delay if the "signal chain" gets too long - CoroutineManager.StartCoroutine(DelaySignal(signal, connection)); } else { @@ -1972,7 +2055,8 @@ namespace Barotrauma //wait one frame yield return CoroutineStatus.Running; - signal.stepsTaken = 0; + delayedSignals.Remove((signal, connection)); + signal.source = this; connection.SendSignal(signal); @@ -2003,6 +2087,8 @@ namespace Barotrauma public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false) { + if (CampaignInteractionType != CampaignMode.InteractionType.None) { return false; } + bool picked = false, selected = false; #if CLIENT bool hasRequiredSkills = true; @@ -2064,7 +2150,7 @@ namespace Barotrauma if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); } showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; #endif - if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue; + if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) { continue; } if ((ic.CanBePicked && pickHit && ic.Pick(picker)) || (ic.CanBeSelected && selectHit && ic.Select(picker))) { @@ -2074,11 +2160,11 @@ namespace Barotrauma if (picker == Character.Controlled) { GUI.ForceMouseOn(null); } if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (ic.CanBeSelected) selected = true; + if (ic.CanBeSelected) { selected = true; } } } - if (!picked) return false; + if (!picked) { return false; } if (picker != null) { @@ -2345,7 +2431,7 @@ namespace Barotrauma private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly) { - var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); + var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties(); SerializableProperty property = extraData[1] as SerializableProperty; if (property != null) { @@ -2424,11 +2510,16 @@ namespace Barotrauma } } - private CoroutineHandle logPropertyChangeCoroutine; + private List> GetInGameEditableProperties() + { + return GetProperties() + .Where(ce => ce.Second.GetAttribute().IsEditable(this)) + .Union(GetProperties()).ToList(); + } private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null) { - var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); + var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties(); if (allProperties.Count == 0) { return; } int propertyIndex = 0; @@ -2579,18 +2670,31 @@ namespace Barotrauma /// public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { - string name = element.Attribute("name").Value; + string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); - ItemPrefab prefab = ItemPrefab.Find(name, identifier); - - if (prefab == null) + string pendingSwap = element.GetAttributeString("pendingswap", ""); + ItemPrefab appliedSwap = null; + ItemPrefab oldPrefab = null; + if (!string.IsNullOrEmpty(pendingSwap) && Level.Loaded?.Type != LevelData.LevelType.Outpost) { - return null; + oldPrefab = ItemPrefab.Find(name, identifier); + appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap); + identifier = pendingSwap; + pendingSwap = null; } - + + ItemPrefab prefab = ItemPrefab.Find(name, identifier); + if (prefab == null) { return null; } + Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); - if (rect.Width == 0 && rect.Height == 0) + Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2); + if (appliedSwap != null) + { + rect.Width = (int)(prefab.sprite.size.X * prefab.Scale); + rect.Height = (int)(prefab.sprite.size.Y * prefab.Scale); + } + else if (rect.Width == 0 && rect.Height == 0) { rect.Width = (int)(prefab.Size.X * prefab.Scale); rect.Height = (int)(prefab.Size.Y * prefab.Scale); @@ -2599,7 +2703,8 @@ namespace Barotrauma Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false, id: idRemap.GetOffsetId(element)) { Submarine = submarine, - linkedToID = new List() + linkedToID = new List(), + PendingItemSwap = string.IsNullOrEmpty(pendingSwap) ? null : MapEntityPrefab.Find(pendingSwap) as ItemPrefab }; #if SERVER @@ -2609,7 +2714,7 @@ namespace Barotrauma } #endif - foreach (XAttribute attribute in element.Attributes()) + foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes()) { if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) { continue; } bool shouldBeLoaded = false; @@ -2622,14 +2727,14 @@ namespace Barotrauma } } - if (shouldBeLoaded) + if (shouldBeLoaded) { object prevValue = property.GetValue(item); property.TrySetValue(item, attribute.Value); //create network events for properties that differ from the prefab values //(e.g. if a character has an item with modified colors in their inventory) - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType().Any() && - (submarine == null || !submarine.Loading )) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType().Any() && + (submarine == null || !submarine.Loading)) { switch (property.Name) { @@ -2655,48 +2760,111 @@ namespace Barotrauma //if we're overriding a non-overridden item in a sub/assembly xml or vice versa, //use the values from the prefab instead of loading them from the sub/assembly xml - bool usePrefabValues = thisIsOverride != prefab.IsOverride; + bool usePrefabValues = thisIsOverride != prefab.IsOverride || appliedSwap != null; List unloadedComponents = new List(item.components); foreach (XElement subElement in element.Elements()) { - switch (subElement.Name.ToString().ToLowerInvariant()) + switch (subElement.Name.ToString().ToLowerInvariant()) { case "upgrade": - { - var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); - UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); - int level = subElement.GetAttributeInt("level", 1); - if (upgradePrefab != null) { - item.AddUpgrade(new Upgrade(item, upgradePrefab, level, subElement)); + var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); + UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); + int level = subElement.GetAttributeInt("level", 1); + if (upgradePrefab != null) + { + item.AddUpgrade(new Upgrade(item, upgradePrefab, level, subElement)); + } + else + { + DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " + + "It's effect will not be applied and won't be saved after the round ends."); + } + break; } - else + default: { - DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " + - "It's effect will not be applied and won't be saved after the round ends."); + ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); + if (component == null) { continue; } + component.Load(subElement, usePrefabValues, idRemap); + unloadedComponents.Remove(component); + break; } - break; - } - default: - { - ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); - if (component == null) { continue; } - component.Load(subElement, usePrefabValues, idRemap); - unloadedComponents.Remove(component); - break; - } } } - if (usePrefabValues) + if (usePrefabValues && appliedSwap == null) { //use prefab scale when overriding a non-overridden item or vice versa item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale"); } - + item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade()); + var availableSwapIds = element.GetAttributeStringArray("availableswaps", new string[0]); + foreach (string swapId in availableSwapIds) + { + ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId); + if (swapPrefab != null) + { + item.AvailableSwaps.Add(swapPrefab); + } + } + + float prevRotation = item.Rotation; if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); } + item.Rotation = prevRotation; + + if (appliedSwap != null) + { + item.SpriteDepth = element.GetAttributeFloat("spritedepth", item.SpriteDepth); + item.SpriteColor = element.GetAttributeColor("spritecolor", item.SpriteColor); + item.Rotation = element.GetAttributeFloat("rotation", item.Rotation); + item.PurchasedNewSwap = element.GetAttributeBool("purchasednewswap", false); + + float scaleRelativeToPrefab = element.GetAttributeFloat(item.scale, "scale", "Scale") / oldPrefab.Scale; + item.Scale *= scaleRelativeToPrefab; + + if (oldPrefab.SwappableItem != null && prefab.SwappableItem != null) + { + Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale"); + oldRelativeOrigin.Y = -oldRelativeOrigin.Y; + oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.rotationRad); + Vector2 oldOrigin = centerPos + oldRelativeOrigin; + + Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale; + relativeOrigin.Y = -relativeOrigin.Y; + relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.rotationRad); + Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin; + + item.rect.Location -= (origin - oldOrigin).ToPoint(); + } + + if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId)) + { + var container = item.GetComponent(); + if (container != null) + { + container.SpawnWithId = appliedSwap.SwappableItem.SpawnWithId; + } + /*string[] splitIdentifier = appliedSwap.SwappableItem.SpawnWithId.Split(','); + foreach (string id in splitIdentifier) + { + ItemPrefab itemToSpawn = ItemPrefab.Find(name: null, identifier: id.Trim()); + if (itemToSpawn == null) + { + DebugConsole.ThrowError($"Failed to spawn an item inside the purchased {item.Name} (could not find an item with the identifier \"{id}\")."); + } + else + { + var spawnedItem = new Item(itemToSpawn, Vector2.Zero, null); + item.OwnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false); + Spawner?.AddToSpawnQueue(itemToSpawn, item.OwnInventory, spawnIfInventoryFull: false); + } + }*/ + } + item.PurchasedNewSwap = false; + } float condition = element.GetAttributeFloat("condition", item.MaxCondition); item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); @@ -2726,12 +2894,22 @@ namespace Barotrauma new XAttribute("identifier", Prefab.Identifier), new XAttribute("ID", ID)); + if (PendingItemSwap != null) + { + element.Add(new XAttribute("pendingswap", PendingItemSwap.Identifier)); + } + if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); } if (Prefab.IsOverride) { element.Add(new XAttribute("isoverride", "true")); } if (FlippedX) { element.Add(new XAttribute("flippedx", true)); } if (FlippedY) { element.Add(new XAttribute("flippedy", true)); } + if (AvailableSwaps.Any()) + { + 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))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index b89df2b70..c7256157a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -3,6 +3,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -39,21 +40,24 @@ namespace Barotrauma public readonly List ItemPrefabs; public int Amount; public readonly float MinCondition; + public readonly float MaxCondition; public readonly bool UseCondition; - public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, bool useCondition) + public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) { ItemPrefabs = new List() { itemPrefab }; Amount = amount; MinCondition = minCondition; + MaxCondition = maxCondition; UseCondition = useCondition; } - public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, bool useCondition) + public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, float maxCondition, bool useCondition) { ItemPrefabs = new List(itemPrefabs); Amount = amount; MinCondition = minCondition; + MaxCondition = maxCondition; UseCondition = useCondition; } } @@ -71,7 +75,7 @@ namespace Barotrauma { TargetItem = itemPrefab; string displayName = element.GetAttributeString("displayname", ""); - DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.Get($"DisplayName.{displayName}"); + DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.GetWithVariable($"DisplayName.{displayName}", "[itemname]", itemPrefab.Name); SuitableFabricatorIdentifiers = element.GetAttributeStringArray("suitablefabricators", new string[0]); @@ -107,6 +111,7 @@ namespace Barotrauma } float minCondition = subElement.GetAttributeFloat("mincondition", 1.0f); + float maxCondition = subElement.GetAttributeFloat("maxcondition", 1.0f); //Substract mincondition from required item's condition or delete it regardless? bool useCondition = subElement.GetAttributeBool("usecondition", true); int count = subElement.GetAttributeInt("count", 1); @@ -119,10 +124,12 @@ namespace Barotrauma continue; } - var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && MathUtils.NearlyEqual(r.MinCondition, minCondition)); + var existing = RequiredItems.Find(r => + r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { - RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition)); + RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, maxCondition, useCondition)); } else { @@ -138,10 +145,13 @@ namespace Barotrauma continue; } - var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems) && MathUtils.NearlyEqual(r.MinCondition, minCondition)); + var existing = RequiredItems.Find(r => + r.ItemPrefabs.SequenceEqual(matchingItems) && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); if (existing == null) { - RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, useCondition)); + RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, maxCondition, useCondition)); } else { @@ -192,6 +202,56 @@ namespace Barotrauma } } + class SwappableItem + { + public int BasePrice { get; } + + public readonly bool CanBeBought; + + public readonly string ReplacementOnUninstall; + + public string SpawnWithId; + + public string SwapIdentifier; + + public readonly Vector2 SwapOrigin; + + public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); + + public readonly Sprite SchematicSprite; + + public int GetPrice(Location location = null) + { + int price = BasePrice; + return location?.GetAdjustedMechanicalCost(price) ?? price; + } + + public SwappableItem(XElement element) + { + BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); + SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); + CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); + ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); + SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); + SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "schematicsprite": + SchematicSprite = new Sprite(subElement); + break; + case "swapconnecteditem": + ConnectedItemsToSwap.Add( + (subElement.GetAttributeString("tag", ""), + subElement.GetAttributeString("swapto", ""))); + break; + } + } + } + } + partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier { private readonly string name; @@ -303,6 +363,14 @@ namespace Barotrauma private set; } + //if true then players can only highlight the item if its targeted for interaction by a campaign event + [Serialize(false, false)] + public bool RequireCampaignInteract + { + get; + private set; + } + //should the camera focus on the item when selected [Serialize(false, false)] public bool FocusOnSelected @@ -448,7 +516,7 @@ namespace Barotrauma [Serialize(null, false)] public string EquipConfirmationText { get; set; } - [Serialize(true, false, description: "Can the item be rotated in the sprite editor.")] + [Serialize(true, false, description: "Can the item be rotated in the submarine editor.")] public bool AllowRotatingInEditor { get; set; } [Serialize(false, false)] @@ -461,6 +529,12 @@ namespace Barotrauma private set; } = new List(); + public SwappableItem SwappableItem + { + get; + private set; + } + /// /// How likely it is for the item to spawn in a level of a given type. /// Key = name of the LevelGenerationParameters (empty string = default value) @@ -670,6 +744,9 @@ namespace Barotrauma //nameidentifier can be used to make multiple items use the same names and descriptions string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + //works the same as nameIdentifier, but just replaces the description string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); @@ -677,11 +754,11 @@ namespace Barotrauma { if (string.IsNullOrEmpty(nameIdentifier)) { - name = TextManager.Get("EntityName." + identifier, true) ?? string.Empty; + name = TextManager.Get("EntityName." + identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } else { - name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } } else if (Category.HasFlag(MapEntityCategory.Legacy)) @@ -830,6 +907,17 @@ namespace Barotrauma break; } #if CLIENT + case "upgradepreviewsprite": + { + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } + UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); + } + break; case "inventoryicon": { string iconFolder = ""; @@ -862,15 +950,15 @@ namespace Barotrauma } break; case "damagedinfectedsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) { - iconFolder = Path.GetDirectoryName(filePath); - } + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } - DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - } + DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } break; case "brokensprite": string brokenSpriteFolder = ""; @@ -963,6 +1051,9 @@ namespace Barotrauma PreferredContainers.Add(preferredContainer); } break; + case "swappableitem": + SwappableItem = new SwappableItem(subElement); + break; case "trigger": Rectangle trigger = new Rectangle(0, 0, 10, 10) { @@ -1144,6 +1235,54 @@ namespace Barotrauma } } + public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) + { + Dictionary priceLocations = new Dictionary(); + foreach (KeyValuePair locationPrice in locationPrices) + { + PriceInfo priceInfo = locationPrice.Value; + + if (priceInfo == null) + { + continue; + } + if (!priceInfo.CanBeBought) + { + continue; + } + if (priceInfo.Price < maxCost || maxCost == 0) + { + priceLocations.Add(locationPrice.Key, priceInfo); + } + } + return priceLocations.ToImmutableDictionary(); + } + + public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + { + Dictionary priceLocations = new Dictionary(); + + if (!CanBeSold && sellingImportant) + { + return priceLocations.ToImmutableDictionary(); + } + + foreach (KeyValuePair locationPrice in locationPrices) + { + PriceInfo priceInfo = locationPrice.Value; + + if (priceInfo == null) + { + continue; + } + if (priceInfo.Price > minCost) + { + priceLocations.Add(locationPrice.Key, priceInfo); + } + } + return priceLocations.ToImmutableDictionary(); + } + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 7806c2708..53c8a8a8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -25,12 +25,12 @@ namespace Barotrauma private readonly float screenColorRange, screenColorDuration; private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; - private bool playTinnitus; - private bool applyFireEffects; - private string[] ignoreFireEffectsForTags; - private bool ignoreCover; - private bool onlyInside; - private bool onlyOutside; + private readonly Color flashColor; + private readonly bool playTinnitus; + private readonly bool applyFireEffects; + private readonly string[] ignoreFireEffectsForTags; + private readonly bool ignoreCover; + private readonly bool onlyInside,onlyOutside; private readonly float flashDuration; private readonly float? flashRange; private readonly string decal; @@ -81,6 +81,7 @@ namespace Barotrauma flash = element.GetAttributeBool("flash", true); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } + flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); @@ -129,7 +130,7 @@ namespace Barotrauma float displayRange = Attack.Range; - Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position; + 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); #if CLIENT @@ -395,7 +396,7 @@ namespace Barotrauma for (int i = 0; i < structure.SectionCount; i++) { float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); - if (distFactor <= 0.0f) continue; + if (distFactor <= 0.0f) { continue; } structure.AddDamage(i, damage * distFactor, attacker); @@ -412,6 +413,19 @@ namespace Barotrauma if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f)) { + if (Level.Loaded?.LevelObjectManager != null) + { + foreach (var levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(worldPosition, worldRange)) + { + if (levelObject.Prefab.TakeLevelWallDamage) + { + float distFactor = 1.0f - (Vector2.Distance(levelObject.WorldPosition, worldPosition) / worldRange); + if (distFactor <= 0.0f) { continue; } + levelObject.AddDamage(levelWallDamage * distFactor, 1.0f, null); + } + } + } + for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index cb846612f..ee66d4483 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -94,7 +94,7 @@ namespace Barotrauma public FireSource(Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) { hull = Hull.FindHull(worldPosition, spawningHull); - if (hull == null || worldPosition.Y < hull.WorldSurface) return; + if (hull == null || worldPosition.Y < hull.WorldSurface) { return; } #if CLIENT if (!isNetworkMessage && GameMain.Client != null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 2703357f7..de7ad710f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -189,6 +189,10 @@ namespace Barotrauma if (roomName == value) { return; } roomName = value; DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName; + if (!IsWetRoom && ForceAsWetRoom) + { + IsWetRoom = true; + } } } @@ -329,6 +333,42 @@ namespace Barotrauma } } + private bool ForceAsWetRoom => + roomName != null && ( + roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || + roomName.Contains("bilge", StringComparison.OrdinalIgnoreCase) || + roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)); + + private bool isWetRoom; + [Editable, Serialize(false, true, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] + public bool IsWetRoom + { + get { return isWetRoom; } + set + { + isWetRoom = value; + if (ForceAsWetRoom) + { + isWetRoom = true; + } + } + } + + private bool avoidStaying; + [Editable, Serialize(false, true, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")] + public bool AvoidStaying + { + get { return avoidStaying || IsWetRoom; } + set + { + avoidStaying = value; + if (IsWetRoom) + { + avoidStaying = true; + } + } + } + public float WaterPercentage => MathUtils.Percentage(WaterVolume, Volume); public float OxygenPercentage @@ -534,6 +574,9 @@ namespace Barotrauma Pressure = rect.Y - rect.Height + waterVolume / rect.Width; BallastFlora?.OnMapLoaded(); +#if CLIENT + lastAmbientLightEditTime = 0.0; +#endif } public void AddToGrid(Submarine submarine) @@ -643,6 +686,11 @@ namespace Barotrauma public void AddFireSource(FireSource fireSource) { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + //clients aren't allowed to create fire sources in hulls whose IDs have been freed (dynamic hulls between docking ports), because they can't be synced + if (IdFreed) { return; } + } if (fireSource is DummyFireSource dummyFire) { FakeFireSources.Add(dummyFire); @@ -705,19 +753,22 @@ namespace Barotrauma Oxygen -= OxygenDeteriorationSpeed * deltaTime; - if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + if (FakeFireSources.Count > 0) { - for (int i = FakeFireSources.Count - 1; i >= 0; i--) + if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) { - if (FakeFireSources[i].CausedByPsychosis) + for (int i = FakeFireSources.Count - 1; i >= 0; i--) { - FakeFireSources[i].Remove(); + if (FakeFireSources[i].CausedByPsychosis) + { + FakeFireSources[i].Remove(); + } } } + FireSource.UpdateAll(FakeFireSources, deltaTime); } FireSource.UpdateAll(FireSources, deltaTime); - FireSource.UpdateAll(FakeFireSources, deltaTime); foreach (Decal decal in decals) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs index 5f1b94581..bbaad892e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs @@ -12,7 +12,7 @@ namespace Barotrauma interface IIgnorable : ISpatialEntity { - bool IgnoreByAI { get; } + bool IgnoreByAI(Character character); bool OrderedToBeIgnored { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 8ce97ea4d..21f78b9af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -36,7 +36,7 @@ namespace Barotrauma public Rectangle Bounds; - public ItemAssemblyPrefab(string filePath) + public ItemAssemblyPrefab(string filePath, bool allowOverwrite = false) { FilePath = filePath; XDocument doc = XMLExtensions.TryLoadXml(filePath); @@ -113,6 +113,10 @@ namespace Barotrauma new Rectangle(0, 0, 1, 1) : new Rectangle(minX, minY, maxX - minX, maxY - minY); + if (allowOverwrite && Prefabs.ContainsKey(identifier)) + { + Prefabs.Remove(Prefabs[identifier]); + } Prefabs.Add(this, doc.Root.IsOverride()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 50e8b87c2..382c0a8ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -35,7 +35,7 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20, + BeaconStation = 0x20, // Not used anywhere Abyss = 0x40, AbyssCave = 0x80 } @@ -325,9 +325,11 @@ namespace Barotrauma get { return LevelData.Seed; } } + + public static float? ForcedDifficulty; public float Difficulty { - get { return LevelData.Difficulty; } + get { return ForcedDifficulty ?? LevelData.Difficulty; } } public LevelData.LevelType Type @@ -387,7 +389,7 @@ namespace Barotrauma private void Generate(bool mirror) { - if (Loaded != null) { Loaded.Remove(); } + Loaded?.Remove(); Loaded = this; Generating = true; @@ -1152,7 +1154,7 @@ namespace Barotrauma } CreateWrecks(); - CreateBeaconStation(cells); + CreateBeaconStation(); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -1892,26 +1894,45 @@ namespace Barotrauma private void CalculateTunnelDistanceField(int density) { distanceField = new List<(Point point, double distance)>(); - for (int x = 0; x < Size.X; x += density) + + if (Mirrored) { - for (int y = 0; y < Size.Y; y += density) + for (int x = Size.X - 1; x >= 0; x -= density) { - Point point = new Point(x, y); - double shortestDistSqr = double.PositiveInfinity; - foreach (Tunnel tunnel in Tunnels) + for (int y = 0; y < Size.Y; y += density) { - for (int i = 1; i < tunnel.Nodes.Count; i++) - { - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); - } + addPoint(x, y); } - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom)); - distanceField.Add((point, Math.Sqrt(shortestDistSqr))); } } + else + { + for (int x = 0; x < Size.X; x += density) + { + for (int y = 0; y < Size.Y; y += density) + { + addPoint(x, y); + } + } + } + + void addPoint(int x, int y) + { + Point point = new Point(x, y); + double shortestDistSqr = double.PositiveInfinity; + foreach (Tunnel tunnel in Tunnels) + { + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); + } + } + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom)); + distanceField.Add((point, Math.Sqrt(shortestDistSqr))); + } } private double GetDistToTunnel(Vector2 position, Tunnel tunnel) @@ -2710,14 +2731,21 @@ namespace Barotrauma return position; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null) + public bool TryGetInterestingPositionAwayFromPoint(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Vector2 awayPoint, float minDistFromPoint, Func filter = null) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, filter); + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, awayPoint, minDistFromPoint, filter); position = pos.ToVector2(); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Func filter = null) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null) + { + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, Vector2.Zero, minDistFromPoint: 0, filter); + position = pos.ToVector2(); + return success; + } + + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Vector2 awayPoint, float minDistFromPoint = 0f, Func filter = null) { if (!PositionsOfInterest.Any()) { @@ -2755,6 +2783,11 @@ namespace Barotrauma farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs); } } + if (minDistFromPoint > 0.0f) + { + farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), awayPoint) < minDistFromPoint * minDistFromPoint); + } + if (!farEnoughPositions.Any()) { string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace(); @@ -2826,7 +2859,7 @@ namespace Barotrauma if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); - Debug.Assert(t < 1.0f); + Debug.Assert(t <= 1.0f); t = MathHelper.Clamp(t, 0.0f, 1.0f); float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); @@ -2993,20 +3026,30 @@ namespace Barotrauma return originalTag + "_" + shortSeed; } + public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist); + public bool IsCloseToEnd(Vector2 position, float minDist) => IsCloseToEnd(position.ToPoint(), minDist); + + public bool IsCloseToStart(Point position, float minDist) + { + return MathUtils.LineSegmentToPointDistanceSquared(StartPosition.ToPoint(), StartExitPosition.ToPoint(), position) < minDist * minDist; + } + + public bool IsCloseToEnd(Point position, float minDist) + { + return MathUtils.LineSegmentToPointDistanceSquared(EndPosition.ToPoint(), EndExitPosition.ToPoint(), position) < minDist * minDist; + } + private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) { var tempSW = new Stopwatch(); // Min distance between a sub and the start/end/other sub. float minDistance = Sonar.DefaultSonarRange; - float squaredMinDistance = minDistance * minDistance; - Vector2 start = startPosition.ToVector2(); - Vector2 end = endPosition.ToVector2(); var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && - Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance && - Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList(); + !IsCloseToStart(wp.WorldPosition, minDistance) && + !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); var subDoc = SubmarineInfo.OpenFile(contentFile.Path); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); @@ -3094,12 +3137,11 @@ namespace Barotrauma sub.SetPosition(spawnPoint); wreckPositions.Add(sub, positions); blockedRects.Add(sub, rects); - return sub; } else { - DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds.ToString()} (ms).", Color.Red); + DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).", Color.Red); return null; } @@ -3150,7 +3192,7 @@ namespace Barotrauma else { var sp = spawnPoint; - if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance)) + if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance)) { Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s)."); return false; @@ -3306,18 +3348,25 @@ namespace Barotrauma } wreckFiles.Shuffle(Rand.RandSync.Server); - int wreckCount = Math.Min(Loaded.GenerationParams.WreckCount, wreckFiles.Count); + int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count); + int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count); + int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.Server); + + if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false) + { + wreckCount = Math.Max(wreckCount, 1); + } + Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { ContentFile contentFile = wreckFiles[i]; if (contentFile == null) { continue; } string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); - // For storing the translations. Used only for debugging. SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck); } totalSW.Stop(); - Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds.ToString()} (ms)"); + Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); } private bool HasStartOutpost() @@ -3365,11 +3414,8 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - if (Submarine.MainSubs.Length > 1 && Submarine.MainSubs[0] != null && Submarine.MainSubs[1] != null) - { - continue; - } - + if (GameMain.GameSession?.GameMode is PvPMode) { continue; } + bool isStart = (i == 0) == !Mirrored; if (isStart) { @@ -3527,7 +3573,7 @@ namespace Barotrauma { spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); } - outpost.SetPosition(spawnPos); + outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); if ((i == 0) == !Mirrored) { StartOutpost = outpost; @@ -3550,7 +3596,7 @@ namespace Barotrauma } } - private void CreateBeaconStation(List mainPath) + private void CreateBeaconStation() { if (!LevelData.HasBeaconStation) { return; } var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 3905348a3..4aec34f52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -496,8 +496,11 @@ namespace Barotrauma [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } + [Serialize(1, true, 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, true, 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 WreckCount { get; set; } + public int MaxWreckCount { get; set; } // TODO: Move the wreck parameters under a separate class? #region Wreck parameters diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 652a65e26..673ce3a7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class LevelObject : ISpatialEntity + partial class LevelObject : ISpatialEntity, IDamageable, ISerializableEntity { public readonly LevelObjectPrefab Prefab; public Vector3 Position; @@ -21,6 +21,8 @@ namespace Barotrauma private int spriteIndex; + protected bool tookDamage; + public LevelObjectPrefab ActivePrefab; public PhysicsBody PhysicsBody @@ -37,11 +39,15 @@ namespace Barotrauma public bool NeedsNetworkSyncing { - get { return Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing); } + get + { + return tookDamage || (Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing)); + } set { if (Triggers == null) { return; } - Triggers.ForEach(t => t.NeedsNetworkSyncing = false); + Triggers.ForEach(t => t.NeedsNetworkSyncing = false); + tookDamage = false; } } @@ -50,6 +56,12 @@ namespace Barotrauma get; private set; } + public float Health + { + get; + private set; + } + public Sprite Sprite { get @@ -67,6 +79,10 @@ namespace Barotrauma public Submarine Submarine => null; + public string Name => Prefab?.Name ?? "LevelObject (null)"; + + public Dictionary SerializableProperties { get; } = new Dictionary(); + public Level.Cave ParentCave; public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f) @@ -75,6 +91,7 @@ namespace Barotrauma Position = position; Scale = scale; Rotation = rotation; + Health = prefab.Health; spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1; @@ -89,10 +106,13 @@ namespace Barotrauma if (PhysicsBody != null) { + PhysicsBody.UserData = this; PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition, -Rotation); PhysicsBody.BodyType = BodyType.Static; PhysicsBody.CollisionCategories = Physics.CollisionLevel; - PhysicsBody.CollidesWith = Physics.CollisionWall | Physics.CollisionCharacter; + PhysicsBody.CollidesWith = Prefab.TakeLevelWallDamage? + Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionProjectile : + Physics.CollisionWall | Physics.CollisionCharacter; } foreach (XElement triggerElement in prefab.LevelTriggerElements) @@ -111,6 +131,10 @@ namespace Barotrauma } var newTrigger = new LevelTrigger(triggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale, prefab.Name); + if (newTrigger.PhysicsBody != null) + { + newTrigger.PhysicsBody.UserData = this; + } int parentTriggerIndex = prefab.LevelTriggerElements.IndexOf(triggerElement.Parent); if (parentTriggerIndex > -1) { newTrigger.ParentTrigger = Triggers[parentTriggerIndex]; } Triggers.Add(newTrigger); @@ -135,7 +159,54 @@ namespace Barotrauma } partial void InitProjSpecific(); - + + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + { + if (Health <= 0.0f) { return new AttackResult(0.0f); } + + float damage = 0.0f; + if (Prefab.TakeLevelWallDamage) + { + damage += attack.GetLevelWallDamage(deltaTime); + } + damage = Math.Max(Health, damage); + AddDamage(damage, deltaTime, attacker); + return new AttackResult(damage); + } + + public void AddDamage(float damage, float deltaTime, Entity attacker, bool isNetworkEvent = false) + { + if (Health <= 0.0f) { return; } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkEvent) + { + return; + } + tookDamage |= !MathUtils.NearlyEqual(damage, 0.0f); + Health -= damage; + if (Health <= 0.0f) + { +#if CLIENT + if (GameMain.GameSession?.Level?.LevelObjectManager != null) + { + GameMain.GameSession.Level.LevelObjectManager.ForceRefreshVisibleObjects = true; + } +#endif + if (PhysicsBody != null) + { + PhysicsBody.Enabled = false; + } + foreach (LevelTrigger trigger in Triggers) + { + trigger.PhysicsBody.Enabled = false; + foreach (StatusEffect effect in trigger.StatusEffects) + { + if (effect.type != ActionType.OnBroken) { continue; } + effect.Apply(effect.type, deltaTime, attacker, this, worldPosition: WorldPosition); + } + } + } + } + public Vector2 LocalToWorld(Vector2 localPosition, float swingState = 0.0f) { Vector2 emitterPos = localPosition * Scale; @@ -169,6 +240,10 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg, Client c) { if (Triggers == null) { return; } + if (Prefab.TakeLevelWallDamage) + { + msg.WriteRangedSingle(MathHelper.Clamp(Health, 0.0f, Prefab.Health), 0.0f, Prefab.Health, 8); + } for (int j = 0; j < Triggers.Count; j++) { if (!Triggers[j].UseNetworkSyncing) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 238743d34..e4288f792 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -21,6 +21,12 @@ namespace Barotrauma private List updateableObjects; private List[,] objectGrid; + public float GlobalForceDecreaseTimer + { + get; + private set; + } + public LevelObjectManager() : base(null, Entity.NullEntityID) { } @@ -130,11 +136,16 @@ namespace Barotrauma if (prefab == null) { continue; } if (!suitableSpawnPositions.ContainsKey(prefab)) { + float minDistance = level.Size.X * 0.2f; + suitableSpawnPositions.Add(prefab, availableSpawnPositions.Where(sp => sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) && - sp.Length >= prefab.MinSurfaceWidth && + sp.Length >= prefab.MinSurfaceWidth && + (prefab.AllowAtStart || !level.IsCloseToStart(sp.GraphEdge.Center, minDistance)) && + (prefab.AllowAtEnd || !level.IsCloseToEnd(sp.GraphEdge.Center, minDistance)) && (sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList()); + spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } @@ -422,10 +433,10 @@ namespace Barotrauma public IEnumerable GetAllObjects(Vector2 worldPosition, float radius) { var minIndices = GetGridIndices(worldPosition - Vector2.One * radius); - if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) return Enumerable.Empty(); + if (minIndices.X >= objectGrid.GetLength(0) || minIndices.Y >= objectGrid.GetLength(1)) { return Enumerable.Empty(); } var maxIndices = GetGridIndices(worldPosition + Vector2.One * radius); - if (maxIndices.X < 0 || maxIndices.Y < 0) return Enumerable.Empty(); + if (maxIndices.X < 0 || maxIndices.Y < 0) { return Enumerable.Empty(); } minIndices.X = Math.Max(0, minIndices.X); minIndices.Y = Math.Max(0, minIndices.Y); @@ -440,6 +451,7 @@ namespace Barotrauma if (objectGrid[x, y] == null) { continue; } foreach (LevelObject obj in objectGrid[x, y]) { + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } objectsInRange.Add(obj); } } @@ -484,6 +496,12 @@ namespace Barotrauma public void Update(float deltaTime) { + GlobalForceDecreaseTimer += deltaTime; + if (GlobalForceDecreaseTimer > 1000000.0f) + { + GlobalForceDecreaseTimer = 0.0f; + } + foreach (LevelObject obj in updateableObjects) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -496,6 +514,7 @@ namespace Barotrauma obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } if (obj.Triggers != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 139837000..b9b83f5d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -176,6 +176,20 @@ namespace Barotrauma private set; } + [Editable, Serialize(true, true, description: "Can the object be placed near the start of the level.")] + public bool AllowAtStart + { + get; + private set; + } + + [Editable, Serialize(true, true, description: "Can the object be placed near the end of the level.")] + public bool AllowAtEnd + { + get; + private set; + } + [Serialize(0.0f, true, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] /// /// Minimum length of a graph edge the object can spawn on. @@ -250,6 +264,27 @@ namespace Barotrauma private set; } + [Serialize(false, true, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] + public bool TakeLevelWallDamage + { + get; + private set; + } + + [Serialize(false, true), Editable] + public bool HideWhenBroken + { + get; + private set; + } + + [Serialize(100.0f, true), Editable] + public float Health + { + get; + private set; + } + public string Identifier { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 3fbe4be14..6259d240d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -38,6 +38,10 @@ namespace Barotrauma /// Effects applied to entities that are inside the trigger /// private readonly List statusEffects = new List(); + public IEnumerable StatusEffects + { + get { return statusEffects; } + } /// /// Attacks applied to entities that are inside the trigger @@ -140,6 +144,11 @@ namespace Barotrauma get; private set; } + public float GlobalForceDecreaseInterval + { + get; + private set; + } private readonly TriggerForceMode forceMode; public TriggerForceMode ForceMode @@ -200,7 +209,7 @@ namespace Barotrauma PhysicsBody = new PhysicsBody(element, scale) { CollisionCategories = Physics.CollisionLevel, - CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall + CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall, }; PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision; PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation; @@ -234,6 +243,7 @@ namespace Barotrauma ForceFluctuationInterval = element.GetAttributeFloat("forcefluctuationinterval", 0.01f); ForceFluctuationStrength = Math.Max(element.GetAttributeFloat("forcefluctuationstrength", 0.0f), 0.0f); ForceFalloff = element.GetAttributeBool("forcefalloff", true); + GlobalForceDecreaseInterval = element.GetAttributeFloat("globalforcedecreaseinterval", 0.0f); ForceVelocityLimit = ConvertUnits.ToSimUnits(element.GetAttributeFloat("forcevelocitylimit", float.MaxValue)); string forceModeStr = element.GetAttributeString("forcemode", "Force"); @@ -434,6 +444,8 @@ namespace Barotrauma } } + private readonly List targets = new List(); + public void Update(float deltaTime) { if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } @@ -457,7 +469,13 @@ namespace Barotrauma if (!UseNetworkSyncing || isNotClient) { - if (ForceFluctuationStrength > 0.0f) + if (GlobalForceDecreaseInterval > 0.0f && Level.Loaded?.LevelObjectManager != null && + Level.Loaded.LevelObjectManager.GlobalForceDecreaseTimer % (GlobalForceDecreaseInterval * 2) < GlobalForceDecreaseInterval) + { + NeedsNetworkSyncing |= currentForceFluctuation > 0.0f; + currentForceFluctuation = 0.0f; + } + else if (ForceFluctuationStrength > 0.0f) { //no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any()) @@ -509,6 +527,7 @@ namespace Barotrauma { foreach (StatusEffect effect in statusEffects) { + if (effect.type == ActionType.OnBroken) { continue; } Vector2? position = null; if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; } if (triggerer is Character character) @@ -533,8 +552,8 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); - effect.GetNearbyTargets(worldPosition, targets); + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); effect.Apply(effect.type, deltaTime, triggerer, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 62e9e02ca..afc15291e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -354,7 +354,7 @@ namespace Barotrauma } } - sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition); + sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition, forceUndockFromStaticSubmarines: false); sub.Submarine = Submarine; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index f5d8712f3..d8aa3610e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -13,14 +13,14 @@ namespace Barotrauma public class TakenItem { public readonly ushort OriginalID; - public readonly ushort OriginalContainerID; public readonly ushort ModuleIndex; public readonly string Identifier; + public readonly int OriginalContainerIndex; - public TakenItem(string identifier, UInt16 originalID, UInt16 originalContainerID, ushort moduleIndex) + public TakenItem(string identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex) { OriginalID = originalID; - OriginalContainerID = originalContainerID; + OriginalContainerIndex = originalContainerIndex; ModuleIndex = moduleIndex; Identifier = identifier; } @@ -29,11 +29,7 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items"); - if (item.OriginalContainerID != Entity.NullEntityID) - { - OriginalContainerID = item.OriginalContainerID; - } - + OriginalContainerIndex = item.OriginalContainerIndex; OriginalID = item.ID; ModuleIndex = (ushort) item.OriginalModuleIndex; Identifier = item.prefab.Identifier; @@ -41,14 +37,14 @@ namespace Barotrauma public bool IsEqual(TakenItem obj) { - return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; + return obj.OriginalID == OriginalID && obj.OriginalContainerIndex == OriginalContainerIndex && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; } public bool Matches(Item item) { - if (item.OriginalContainerID != Entity.NullEntityID) + if (item.OriginalContainerIndex != Entity.NullEntityID) { - return item.OriginalContainerID == OriginalContainerID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; } else { @@ -181,27 +177,54 @@ namespace Barotrauma } } - public Mission SelectedMission + private readonly List selectedMissions = new List(); + public IEnumerable SelectedMissions { - get; - set; + get + { + selectedMissions.RemoveAll(m => !availableMissions.Contains(m)); + return selectedMissions; + } } - public int SelectedMissionIndex + public void SelectMission(Mission mission) { - get + if (!SelectedMissions.Contains(mission) && mission != null) { - if (SelectedMission == null) { return -1; } - return availableMissions.IndexOf(SelectedMission); + selectedMissions.Add(mission); } - set + } + + public void DeselectMission(Mission mission) + { + selectedMissions.Remove(mission); + } + + + public List GetSelectedMissionIndices() + { + List selectedMissionIndices = new List(); + foreach (Mission mission in SelectedMissions) { - if (value < 0 || value >= AvailableMissions.Count()) + if (availableMissions.Contains(mission)) { - SelectedMission = null; - return; + selectedMissionIndices.Add(availableMissions.IndexOf(mission)); } - SelectedMission = availableMissions[value]; + } + return selectedMissionIndices; + } + + public void SetSelectedMissionIndices(IEnumerable missionIndices) + { + selectedMissions.Clear(); + foreach (int missionIndex in missionIndices) + { + if (missionIndex < 0 || missionIndex >= availableMissions.Count) + { + DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})"); + break; + } + selectedMissions.Add(availableMissions[missionIndex]); } } @@ -322,9 +345,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\""); continue; } - if (!ushort.TryParse(takenItemSplit[2], out ushort containerId)) + if (!int.TryParse(takenItemSplit[2], out int containerIndex)) { - DebugConsole.ThrowError($"Error in saved location: could not parse taken container id \"{takenItemSplit[2]}\""); + DebugConsole.ThrowError($"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\""); continue; } if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex)) @@ -332,7 +355,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\""); continue; } - takenItems.Add(new TakenItem(takenItemSplit[0], id, containerId, moduleIndex)); + takenItems.Add(new TakenItem(takenItemSplit[0], id, containerIndex, moduleIndex)); } killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet(); @@ -415,6 +438,12 @@ namespace Barotrauma { if (newType == Type) { return; } + if (newType == null) + { + DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } + DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; @@ -533,8 +562,28 @@ namespace Barotrauma //prefer connections that haven't been passed through, and connections with fewer available missions connection = ToolBox.SelectWeightedRandom( suitableConnections.ToList(), - suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(), - Rand.RandSync.Unsynced); + suitableConnections.Select(c => GetConnectionWeight(this, c)).ToList(), + Rand.RandSync.Unsynced); + + static float GetConnectionWeight(Location location, LocationConnection c) + { + float weight = c.Passed ? 1.0f : 5.0f; + Location destination = c.OtherLocation(location); + if (destination != null) + { + if (destination.MapPosition.X > location.MapPosition.X) { weight *= 2.0f; } + int missionCount = location.availableMissions.Count(m => m.Locations.Contains(destination)); + if (missionCount > 0) + { + weight /= missionCount * 2; + } + if (destination.IsRadiated()) + { + weight *= 0.001f; + } + } + return weight; + } return InstantiateMission(prefab, connection); } @@ -542,14 +591,14 @@ namespace Barotrauma private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection) { Location destination = connection.OtherLocation(this); - var mission = prefab.Instantiate(new Location[] { this, destination }); + var mission = prefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); mission.AdjustLevelData(connection.LevelData); return mission; } private Mission InstantiateMission(MissionPrefab prefab) { - var mission = prefab.Instantiate(new Location[] { this, this }); + var mission = prefab.Instantiate(new Location[] { this, this }, Submarine.MainSub); mission.AdjustLevelData(LevelData); return mission; } @@ -557,6 +606,7 @@ namespace Barotrauma public void InstantiateLoadedMissions(Map map) { availableMissions.Clear(); + selectedMissions.Clear(); if (loadedMissions != null && loadedMissions.Any()) { foreach (LoadedMission loadedMission in loadedMissions) @@ -570,9 +620,9 @@ namespace Barotrauma { destination = Connections.First().OtherLocation(this); } - var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }); + var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub); availableMissions.Add(mission); - if (loadedMission.SelectedMission) { SelectedMission = mission; } + if (loadedMission.SelectedMission) { selectedMissions.Add(mission); } } loadedMissions = null; } @@ -596,7 +646,7 @@ namespace Barotrauma public void ClearMissions() { availableMissions.Clear(); - SelectedMissionIndex = -1; + selectedMissions.Clear(); } public bool HasOutpost() @@ -1107,7 +1157,7 @@ namespace Barotrauma { locationElement.Add(new XAttribute( "takenitems", - string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerID + ";" + it.ModuleIndex)))); + string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerIndex + ";" + it.ModuleIndex)))); } if (killedCharacterIdentifiers.Any()) { @@ -1164,7 +1214,7 @@ namespace Barotrauma missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), new XAttribute("destinationindex", i), - new XAttribute("selected", mission == SelectedMission))); + new XAttribute("selected", selectedMissions.Contains(mission)))); } locationElement.Add(missionsElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 5f27063a4..77cd99589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -24,7 +24,7 @@ namespace Barotrauma /// From -> To /// public Action OnLocationChanged; - public Action OnMissionSelected; + public Action> OnMissionsSelected; public Location EndLocation { get; private set; } @@ -44,9 +44,9 @@ namespace Barotrauma get { return Locations.IndexOf(SelectedLocation); } } - public int SelectedMissionIndex + public IEnumerable GetSelectedMissionIndices() { - get { return SelectedConnection == null ? -1 : CurrentLocation.SelectedMissionIndex; } + return SelectedConnection == null ? Enumerable.Empty() : CurrentLocation.GetSelectedMissionIndices(); } public LocationConnection SelectedConnection { get; private set; } @@ -388,8 +388,14 @@ namespace Barotrauma } } - LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones]; - foreach (var connection in Connections) + List[] connectionsBetweenZones = new List[generationParams.DifficultyZones]; + for (int i = 0; i < generationParams.DifficultyZones; i++) + { + connectionsBetweenZones[i] = new List(); + } + var shuffledConnections = Connections.ToList(); + shuffledConnections.Shuffle(Rand.RandSync.Server); + foreach (var connection in shuffledConnections) { int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X); int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X); @@ -401,17 +407,25 @@ namespace Barotrauma zone1 = temp; } - if (connectionsBetweenZones[zone1] == null) + if (generationParams.GateCount[zone1] == 0) { continue; } + + if (!connectionsBetweenZones[zone1].Any()) { - connectionsBetweenZones[zone1] = connection; + connectionsBetweenZones[zone1].Add(connection); } - else + else if (generationParams.GateCount[zone1] == 1) { - if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2)) + //if there's only one connection, place it at the center of the map + if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].First().CenterPos.Y - Height / 2)) { - connectionsBetweenZones[zone1] = connection; + connectionsBetweenZones[zone1].Clear(); + connectionsBetweenZones[zone1].Add(connection); } } + else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1]) + { + connectionsBetweenZones[zone1].Add(connection); + } } for (int i = Connections.Count - 1; i >= 0; i--) @@ -421,7 +435,9 @@ namespace Barotrauma if (zone1 == zone2) { continue; } if (zone1 == generationParams.DifficultyZones || zone2 == generationParams.DifficultyZones) { continue; } - if (!connectionsBetweenZones.Contains(Connections[i])) + if (generationParams.GateCount[Math.Min(zone1, zone2)] == 0) { continue; } + + if (!connectionsBetweenZones[Math.Min(zone1, zone2)].Contains(Connections[i])) { Connections.RemoveAt(i); } @@ -756,7 +772,7 @@ namespace Barotrauma OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); } - public void SelectMission(int missionIndex) + public void SelectMission(IEnumerable missionIndices) { if (CurrentLocation == null) { @@ -765,23 +781,24 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("Map.SelectMission:CurrentLocationNotSet", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - CurrentLocation.SelectedMissionIndex = missionIndex; - if (CurrentLocation.SelectedMission == null) { return; } + CurrentLocation.SetSelectedMissionIndices(missionIndices); - if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation || - CurrentLocation.SelectedMission.Locations[1] != CurrentLocation) + foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList()) { - if (SelectedConnection == null) { return; } - //the destination must be the same as the destination of the mission - if (CurrentLocation.SelectedMission != null && - CurrentLocation.SelectedMission.Locations[1] != SelectedLocation) + if (selectedMission.Locations[0] != CurrentLocation || + selectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.SelectedMissionIndex = -1; + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (selectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.DeselectMission(selectedMission); + } } } - OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission); + OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions); } public void SelectRandomLocation(bool preferUndiscovered) @@ -977,6 +994,12 @@ namespace Barotrauma string prevName = location.Name; var newType = LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)); + if (newType == null) + { + DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); + return; + } + if (newType.OutpostTeam != location.Type.OutpostTeam || newType.HasOutpost != location.Type.HasOutpost) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index 6e9194dd6..3c47acbd5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -67,6 +67,8 @@ namespace Barotrauma [Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] public float ConnectionIndicatorDisplacementMultiplier { get; set; } + public int[] GateCount { get; private set; } + #if CLIENT [Serialize(0.75f, true), Editable(DecimalCount = 2)] @@ -201,6 +203,16 @@ namespace Barotrauma { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + GateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null); + if (GateCount == null) + { + GateCount = new int[DifficultyZones]; + for (int i = 0; i < DifficultyZones; i++) + { + GateCount[i] = 1; + } + } + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index d10ae10d8..fbe5d775f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -81,6 +81,7 @@ namespace Barotrauma if (location.Type.HasOutpost && !wasCritical && location.IsCriticallyRadiated()) { + location.ClearMissions(); amountOfOutposts--; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 873ab9a84..61e14591b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -243,7 +243,7 @@ namespace Barotrauma /// public int OriginalModuleIndex = -1; - public UInt16 OriginalContainerID; + public int OriginalContainerIndex = -1; public virtual string Name { @@ -280,7 +280,7 @@ namespace Barotrauma public void ResolveLinks(IdRemap childRemap) { if (unresolvedLinkedToID == null) { return; } - for (int i=0;i(); - if (door != null) { door.RefreshLinkedGap(); } + door?.RefreshLinkedGap(); var cloneWire = cloneItem.GetComponent(); if (cloneWire == null) continue; @@ -509,9 +509,9 @@ namespace Barotrauma mapEntityList.Remove(this); #if CLIENT - if (selectedList.Contains(this)) + if (SelectedList.Contains(this)) { - selectedList = selectedList.FindAll(e => e != this); + SelectedList = SelectedList.Where(e => e != this).ToHashSet(); } #endif @@ -649,7 +649,10 @@ namespace Barotrauma else { object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap }); - if (newEntity != null) entities.Add((MapEntity)newEntity); + if (newEntity != null) + { + entities.Add((MapEntity)newEntity); + } } } catch (TargetInvocationException e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index bef44a09f..2e07a4c11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -186,7 +186,8 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList) { if (hull.Submarine != sub) { continue; } - if (hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(hull.RoomName) || + hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) { hull.RoomName = hull.CreateRoomName(); } @@ -1436,6 +1437,7 @@ namespace Barotrauma var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, true); npc.TeamID = CharacterTeamType.FriendlyNPC; + npc.Prefab = humanPrefab; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) { outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); @@ -1447,7 +1449,7 @@ namespace Barotrauma } else { - npc.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier; + npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); } humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 6c7fe54dc..fd433482a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -27,7 +27,7 @@ namespace Barotrauma public Submarine Submarine => Wall.Submarine; public Rectangle WorldRect => Submarine == null ? rect : new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height); - public bool IgnoreByAI => OrderedToBeIgnored; + public bool IgnoreByAI(Character character) => OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public WallSection(Rectangle rect, Structure wall, float damage = 0.0f) @@ -53,7 +53,16 @@ namespace Barotrauma //dimensions of the wall sections' physics bodies (only used for debug rendering) private readonly List bodyDebugDimensions = new List(); - public bool Indestructible; +#if DEBUG + [Serialize(false, true), Editable] +#else + [Serialize(false, true)] +#endif + public bool Indestructible + { + get; + set; + } //sections of the wall that are supposed to be rendered public WallSection[] Sections @@ -353,10 +362,7 @@ namespace Barotrauma } #if CLIENT - if (convexHulls!=null) - { - convexHulls.ForEach(x => x.Move(amount)); - } + convexHulls?.ForEach(x => x.Move(amount)); #endif } @@ -801,7 +807,7 @@ namespace Barotrauma public void AddDamage(int sectionIndex, float damage, Character attacker = null) { - if (!Prefab.Body || Prefab.Platform || Indestructible ) { return; } + if (!Prefab.Body || Prefab.Platform || Indestructible) { return; } if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; } @@ -875,13 +881,17 @@ namespace Barotrauma public Vector2 SectionPosition(int sectionIndex, bool world = false) { - if (sectionIndex < 0 || sectionIndex >= Sections.Length) return Vector2.Zero; + if (sectionIndex < 0 || sectionIndex >= Sections.Length) + { + return Vector2.Zero; + } if (Prefab.BodyRotation == 0.0f) { Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f); + if (world && Submarine != null) { sectionPos += Submarine.Position; @@ -900,8 +910,11 @@ namespace Barotrauma { diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight; } - if (FlippedX) diffFromCenter = -diffFromCenter; - + if (FlippedX) + { + diffFromCenter = -diffFromCenter; + } + Vector2 sectionPos = Position + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index d4e620846..380df3975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -250,17 +250,21 @@ namespace Barotrauma var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); if (string.IsNullOrEmpty(sp.originalName)) { if (string.IsNullOrEmpty(nameIdentifier)) { - sp.name = TextManager.Get("EntityName." + sp.identifier, true) ?? string.Empty; + sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } else { - sp.name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 9f81f4ab3..143f60816 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -77,6 +77,7 @@ namespace Barotrauma private static Vector2 lastPickedPosition; private static float lastPickedFraction; + private static Fixture lastPickedFixture; private static Vector2 lastPickedNormal; private Vector2 prevPosition; @@ -99,6 +100,11 @@ namespace Barotrauma get { return lastPickedFraction; } } + public static Fixture LastPickedFixture + { + get { return lastPickedFixture; } + } + public static Vector2 LastPickedNormal { get { return lastPickedNormal; } @@ -368,7 +374,7 @@ namespace Barotrauma public WreckAI WreckAI { get; private set; } public bool CreateWreckAI() { - WreckAI = new WreckAI(this); + WreckAI = WreckAI.Create(this); return WreckAI != null; } @@ -669,6 +675,7 @@ namespace Barotrauma float closestFraction = 1.0f; Vector2 closestNormal = Vector2.Zero; + Fixture closestFixture = null; Body closestBody = null; if (allowInsideFixture) { @@ -682,13 +689,15 @@ namespace Barotrauma closestFraction = 0.0f; closestNormal = Vector2.Normalize(rayEnd - rayStart); - if (fixture.Body != null) closestBody = fixture.Body; + closestFixture = fixture; + if (fixture.Body != null) { closestBody = fixture.Body; } return false; }, ref aabb); if (closestFraction <= 0.0f) { lastPickedPosition = rayStart; lastPickedFraction = closestFraction; + lastPickedFixture = closestFixture; lastPickedNormal = closestNormal; return closestBody; } @@ -702,6 +711,7 @@ namespace Barotrauma { closestFraction = fraction; closestNormal = normal; + closestFixture = fixture; if (fixture.Body != null) closestBody = fixture.Body; } return fraction; @@ -709,6 +719,7 @@ namespace Barotrauma lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction; lastPickedFraction = closestFraction; + lastPickedFixture = closestFixture; lastPickedNormal = closestNormal; return closestBody; @@ -752,6 +763,7 @@ namespace Barotrauma lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction; lastPickedFraction = fraction; lastPickedNormal = normal; + lastPickedFixture = fixture; } //continue return -1; @@ -772,6 +784,7 @@ namespace Barotrauma lastPickedPosition = rayStart; lastPickedFraction = 0.0f; lastPickedNormal = Vector2.Normalize(rayEnd - rayStart); + lastPickedFixture = fixture; bodies.Add(fixture.Body); bodyDist[fixture.Body] = 0.0f; return false; @@ -828,6 +841,7 @@ namespace Barotrauma { Body closestBody = null; float closestFraction = 1.0f; + Fixture closestFixture = null; Vector2 closestNormal = Vector2.Zero; if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f) @@ -847,6 +861,8 @@ namespace Barotrauma if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; } if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData as string == "ruinroom") { return -1; } + //the hulls have solid fixtures in the submarine's world space collider, ignore them + if (fixture.UserData is Hull) { return -1; } if (fixture.Body.UserData is Structure structure) { if (structure.IsPlatform || structure.StairDirection != Direction.None) { return -1; } @@ -861,6 +877,7 @@ namespace Barotrauma { closestBody = fixture.Body; closestFraction = fraction; + closestFixture = fixture; closestNormal = normal; } return closestFraction; @@ -870,6 +887,7 @@ namespace Barotrauma lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction; lastPickedFraction = closestFraction; + lastPickedFixture = closestFixture; lastPickedNormal = closestNormal; return closestBody; } @@ -944,19 +962,23 @@ namespace Barotrauma mapEntity.Move(HiddenSubPosition); } - foreach (Item item in Item.ItemList) + for (int i = 0; i < 2; i++) { - if (bodyItems.Contains(item)) + foreach (Item item in Item.ItemList) { - item.Submarine = this; - if (Position == Vector2.Zero) item.Move(-HiddenSubPosition); + //two passes: flip docking ports on the 2nd pass because the doors need to be correctly flipped for the port's orientation to be determined correctly + if ((item.GetComponent() != null) == (i == 0)) { continue; } + if (bodyItems.Contains(item)) + { + item.Submarine = this; + if (Position == Vector2.Zero) { item.Move(-HiddenSubPosition); } + } + else if (item.Submarine != this) + { + continue; + } + item.FlipX(true); } - else if (item.Submarine != this) - { - continue; - } - - item.FlipX(true); } Item.UpdateHulls(); @@ -1152,7 +1174,7 @@ namespace Barotrauma prevPosition = position; } - public void SetPosition(Vector2 position, List checkd = null) + public void SetPosition(Vector2 position, List checkd = null, bool forceUndockFromStaticSubmarines = true) { if (!MathUtils.IsValid(position)) { return; } @@ -1166,7 +1188,7 @@ namespace Barotrauma foreach (Submarine dockedSub in DockedTo) { - if (dockedSub.PhysicsBody.BodyType == BodyType.Static) + if (dockedSub.PhysicsBody.BodyType == BodyType.Static && forceUndockFromStaticSubmarines) { if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) { @@ -1176,7 +1198,7 @@ namespace Barotrauma } Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - dockedSub.SetPosition(position + expectedLocation.Value, checkd); + dockedSub.SetPosition(position + expectedLocation.Value, checkd, forceUndockFromStaticSubmarines); dockedSub.UpdateTransform(interpolate: false); } } @@ -1238,6 +1260,26 @@ namespace Barotrauma return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); } + public List<(ItemContainer container, int freeSlots)> GetCargoContainers() + { + List<(ItemContainer container, int freeSlots)> containers = new List<(ItemContainer container, int freeSlots)>(); + var connectedSubs = GetConnectedSubs(); + foreach (Item item in Item.ItemList) + { + if (!connectedSubs.Contains(item.Submarine)) { continue; } + if (!item.HasTag("cargocontainer")) { continue; } + var itemContainer = item.GetComponent(); + if (itemContainer == null) { continue; } + int emptySlots = 0; + for (int i = 0; i < itemContainer.Inventory.Capacity; i++) + { + if (itemContainer.Inventory.GetItemAt(i) == null) { emptySlots++; } + } + containers.Add((itemContainer, emptySlots)); + } + return containers; + } + public IEnumerable GetEntities(bool includingConnectedSubs, IEnumerable list) where T : MapEntity { return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); @@ -1527,6 +1569,8 @@ namespace Barotrauma Rectangle dimensions = CalculateDimensions(); element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); + var cargoContainers = GetCargoContainers(); + element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity))); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); @@ -1537,6 +1581,41 @@ namespace Barotrauma Info.OutpostModuleInfo?.Save(element); } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap?.SwappableItem?.ConnectedItemsToSwap == null) { continue; } + foreach (var (requiredTag, swapTo) in item.PendingItemSwap.SwappableItem.ConnectedItemsToSwap) + { + List itemsToSwap = new List(); + itemsToSwap.AddRange(item.linkedTo.Where(lt => (lt as Item)?.HasTag(requiredTag) ?? false).Cast()); + var connectionPanel = item.GetComponent(); + if (connectionPanel != null) + { + foreach (Connection c in connectionPanel.Connections) + { + foreach (var connectedComponent in item.GetConnectedComponentsRecursive(c)) + { + if (!itemsToSwap.Contains(connectedComponent.Item) && connectedComponent.Item.HasTag(requiredTag)) + { + itemsToSwap.Add(connectedComponent.Item); + } + } + } + } + ItemPrefab itemPrefab = ItemPrefab.Find("", swapTo); + if (itemPrefab == null) + { + DebugConsole.ThrowError($"Failed to swap an item connected to \"{item.Name}\" into \"{swapTo}\"."); + continue; + } + foreach (Item itemToSwap in itemsToSwap) + { + itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap; + if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } + } + } + } + foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { if (!e.ShouldBeSaved) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index a81baf888..4c94d8128 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -174,6 +174,11 @@ namespace Barotrauma float simWidth = ConvertUnits.ToSimUnits(width); float simHeight = ConvertUnits.ToSimUnits(height); + if (sub.FlippedX) + { + simPos.X = -simPos.X; + } + if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); @@ -317,19 +322,30 @@ namespace Barotrauma Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height)))); } - if (Position.X < 0) + //hard limit for how far outside the level the sub can go + float maxDist = 200000.0f; + //the force of the current starts to increase exponentially after this point + float exponentialForceIncreaseDist = 150000.0f; + float distance = Position.X < 0 ? Math.Abs(Position.X) : Position.X - Level.Loaded.Size.X; + if (distance > 0) { - float force = Math.Abs(Position.X * 0.5f); - totalForce += Vector2.UnitX * force; - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + if (distance > maxDist) { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f)); + if (Position.X < 0) + { + Body.LinearVelocity = new Vector2(Math.Max(0, Body.LinearVelocity.X), Body.LinearVelocity.Y); + } + else + { + Body.LinearVelocity = new Vector2(Math.Min(0, Body.LinearVelocity.X), Body.LinearVelocity.Y); + } } - } - else - { - float force = (Position.X - Level.Loaded.Size.X) * 0.5f; - totalForce -= Vector2.UnitX * force; + if (distance > exponentialForceIncreaseDist) + { + distance += (float)Math.Pow((distance - exponentialForceIncreaseDist) * 0.01f, 2.0f); + } + float force = distance * 0.5f; + totalForce += (Position.X < 0 ? Vector2.UnitX : -Vector2.UnitX) * force; if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f)); @@ -513,7 +529,7 @@ namespace Barotrauma { return CheckCharacterCollision(contact, character); } - else if (f2.UserData is Items.Components.DockingPort) + else if (f1.UserData is Items.Components.DockingPort || f2.UserData is Items.Components.DockingPort) { return false; } @@ -823,9 +839,9 @@ namespace Barotrauma } #if CLIENT - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + if (Character.Controlled != null && Character.Controlled.Submarine == submarine && Character.Controlled.KnockbackCooldownTimer <= 0.0f) { - GameMain.GameScreen.Cam.Shake = impact * 10.0f; + GameMain.GameScreen.Cam.Shake = Math.Max(impact * 10.0f, GameMain.GameScreen.Cam.Shake); if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player)) { float angularVelocity = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 97c714f73..731c4e0f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -23,7 +23,7 @@ namespace Barotrauma HideInMenus = 2 } - public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation } + public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine } public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver } partial class SubmarineInfo : IDisposable @@ -133,6 +133,12 @@ namespace Barotrauma private set; } + public int CargoCapacity + { + get; + private set; + } + public string FilePath { get; @@ -261,6 +267,7 @@ namespace Barotrauma SubmarineClass = original.SubmarineClass; hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null; Dimensions = original.Dimensions; + CargoCapacity = original.CargoCapacity; FilePath = original.FilePath; RequiredContentPackages = new HashSet(original.RequiredContentPackages); IsFileCorrupted = original.IsFileCorrupted; @@ -323,6 +330,7 @@ namespace Barotrauma Tags = tags; } Dimensions = SubmarineElement.GetAttributeVector2("dimensions", Vector2.Zero); + CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1); RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); @@ -511,10 +519,14 @@ namespace Barotrauma //saving/loading ---------------------------------------------------- public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { - var newElement = new XElement(SubmarineElement.Name, - SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase) && - !string.Equals(a.Name.LocalName, "name", StringComparison.InvariantCultureIgnoreCase)), + var newElement = new XElement( + SubmarineElement.Name, + SubmarineElement.Attributes() + .Where(a => + !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase) && + !string.Equals(a.Name.LocalName, "name", StringComparison.InvariantCultureIgnoreCase)), SubmarineElement.Elements()); + if (Type == SubmarineType.OutpostModule) { OutpostModuleInfo.Save(newElement); @@ -523,7 +535,6 @@ namespace Barotrauma XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); - if (previewImage != null) { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); @@ -574,7 +585,7 @@ namespace Barotrauma var contentPackageSubs = ContentPackage.GetFilesOfType( GameMain.Config.AllEnabledPackages, ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, - ContentType.Wreck, ContentType.BeaconStation); + ContentType.Wreck, ContentType.BeaconStation, ContentType.EnemySubmarine); for (int i = savedSubmarines.Count - 1; i >= 0; i--) { @@ -680,8 +691,6 @@ namespace Barotrauma } } - static readonly string TempFolder = Path.Combine("Submarine", "Temp"); - public static XDocument OpenFile(string file) { return OpenFile(file, out _); @@ -711,7 +720,7 @@ namespace Barotrauma if (extension == ".sub") { - System.IO.Stream stream = null; + System.IO.Stream stream; try { stream = SaveUtil.DecompressFiletoStream(file); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 0d3bf3e32..65d32afa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -192,17 +192,37 @@ namespace Barotrauma float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; - foreach (Hull hull in Hull.hullList) { // Ignore hulls that a human couldn't fit in. // Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually. if (hull.Rect.Height < hullMinHeight) { continue; } - // Don't create waypoints if there's no floor. - Vector2 floorPos = new Vector2(hull.SimPosition.X, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); - Body floor = Submarine.PickBody(hull.SimPosition, floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); + // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor. + Body floor = null; + for (int i = 0; i < 5; i++) + { + float horizontalOffset = 0; + switch (i) + { + case 1: + horizontalOffset = hull.RectWidth * 0.2f; + break; + case 2: + horizontalOffset = hull.RectWidth * 0.4f; + break; + case 3: + horizontalOffset = -hull.RectWidth * 0.2f; + break; + case 4: + horizontalOffset = -hull.RectWidth * 0.4f; + break; + } + horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset); + Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); + floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); + if (floor != null) { break; } + } if (floor == null) { continue; } - // Make sure that the waypoints don't go higher than the halfway of the room. float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; if (hull.Rect.Width < diffFromHullEdge * 3.0f) { @@ -223,12 +243,42 @@ namespace Barotrauma new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } } - } + } + + // Platforms + foreach (Structure platform in Structure.WallList) + { + if (!platform.IsPlatform) { continue; } + float waypointHeight = heightFromFloor; + WayPoint prevWaypoint = null; + for (float x = platform.Rect.X + diffFromHullEdge; x <= platform.Rect.Right - diffFromHullEdge; x += minDist) + { + WayPoint wayPoint = new WayPoint(new Vector2(x, platform.Rect.Y + waypointHeight), SpawnType.Path, submarine); + if (prevWaypoint != null) + { + wayPoint.ConnectTo(prevWaypoint); + } + // If the waypoint is close to hull waypoints, remove it. + if (wayPoint != null) + { + for (int dir = -1; dir <= 1; dir += 2) + { + if (wayPoint.FindClosest(dir, horizontalSearch: true, tolerance: new Vector2(minDist, heightFromFloor), ignored: prevWaypoint.ToEnumerable()) != null) + { + wayPoint.Remove(); + wayPoint = null; + break; + } + } + } + prevWaypoint = wayPoint; + } + } float outSideWaypointInterval = 100.0f; if (submarine.Info.Type != SubmarineType.OutpostModule) { - List outsideWaypoints = new List(); + List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>(); Rectangle borders = Hull.GetBorders(); int originalWidth = borders.Width; @@ -260,7 +310,7 @@ namespace Barotrauma new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition, SpawnType.Path, submarine); - outsideWaypoints.Add(wayPoint); + outsideWaypoints.Add((wayPoint, i)); if (x == borders.X + outSideWaypointInterval) { @@ -284,7 +334,7 @@ namespace Barotrauma new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition, SpawnType.Path, submarine); - outsideWaypoints.Add(wayPoint); + outsideWaypoints.Add((wayPoint, i)); if (y == borders.Y - borders.Height) { @@ -302,8 +352,9 @@ namespace Barotrauma Vector2 center = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition); float halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2); // Try to move the waypoints so that they are near the walls, roughly following the shape of the sub. - foreach (WayPoint wp in outsideWaypoints) + foreach (var wayPoint in outsideWaypoints) { + WayPoint wp = wayPoint.Item1; float xDiff = center.X - wp.SimPosition.X; Vector2 targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y); Body wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine)); @@ -331,8 +382,9 @@ namespace Barotrauma var removals = new List(); WayPoint previous = null; float tooClose = outSideWaypointInterval / 2; - foreach (WayPoint wp in outsideWaypoints) + foreach (var wayPoint in outsideWaypoints) { + WayPoint wp = wayPoint.Item1; if (wp.CurrentHull != null || Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null) { @@ -341,8 +393,9 @@ namespace Barotrauma previous = wp; continue; } - foreach (WayPoint otherWp in outsideWaypoints) + foreach (var otherWayPoint in outsideWaypoints) { + WayPoint otherWp = otherWayPoint.Item1; if (otherWp == wp) { continue; } if (removals.Contains(otherWp)) { continue; } float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position); @@ -356,13 +409,12 @@ namespace Barotrauma } foreach (WayPoint wp in removals) { - outsideWaypoints.Remove(wp); + outsideWaypoints.RemoveAll(w => w.Item1 == wp); wp.Remove(); } - // Connect loose ends (TODO: this sometimes fails, creating the connection to a wrong node) for (int i = 0; i < outsideWaypoints.Count; i++) { - WayPoint current = outsideWaypoints[i]; + WayPoint current = outsideWaypoints[i].Item1; if (current.linkedTo.Count > 1) { continue; } WayPoint next = null; int maxConnections = 2; @@ -371,19 +423,12 @@ namespace Barotrauma { if (current.linkedTo.Count >= maxConnections) { break; } tooFar /= current.linkedTo.Count; - // First try to find a loose end - next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.linkedTo.Count < 2); - // Then accept any connection that not connected to the existing connection - next ??= current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e))); + next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp.Item1 != next && wp.Item1.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.Item1.linkedTo.Count < 2 && wp.Item2 < i); if (next != null) { current.ConnectTo(next); } } - if (current.linkedTo.Count == 1) - { - DebugConsole.ThrowError($"Couldn't automatically link waypoint {current.ID}. You should do it manually."); - } } } @@ -532,7 +577,7 @@ namespace Barotrauma } else { - closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 70), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints); + closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 100), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints); } if (closest == null) { continue; } ladderPoint.ConnectTo(closest); @@ -601,13 +646,20 @@ namespace Barotrauma } } - var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && !w.linkedTo.Any()); - + var orphans = WayPointList.FindAll(w => w.spawnType == SpawnType.Path && w.linkedTo.None()); foreach (WayPoint wp in orphans) { wp.Remove(); } + foreach (WayPoint wp in WayPointList) + { + if (wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2) + { + DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color."); + } + } + //re-disable the bodies of the doors that are supposed to be open foreach (Door door in openDoors) { @@ -617,18 +669,20 @@ namespace Barotrauma return true; } - private WayPoint FindClosestOutside(IEnumerable waypointList, float tolerance, Body ignoredBody = null, IEnumerable ignored = null, Func filter = null) + private WayPoint FindClosestOutside(IEnumerable<(WayPoint, int)> waypointList, float tolerance, Body ignoredBody = null, IEnumerable ignored = null, Func<(WayPoint, int), bool> filter = null) { float closestDist = 0; WayPoint closest = null; - foreach (WayPoint wp in waypointList) + foreach (var wayPoint in waypointList) { + WayPoint wp = wayPoint.Item1; if (wp.SpawnType != SpawnType.Path || wp == this) { continue; } // Ignore if already linked if (linkedTo.Contains(wp)) { continue; } if (ignored != null && ignored.Contains(wp)) { continue; } - if (filter != null && !filter(wp)) { continue; } + if (filter != null && !filter(wayPoint)) { continue; } float sqrDist = Vector2.DistanceSquared(Position, wp.Position); + if (sqrDist > tolerance * tolerance) { continue; } if (closest == null || sqrDist < closestDist) { var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false); @@ -712,14 +766,14 @@ namespace Barotrauma if (!wayPoint2.linkedTo.Contains(this)) { wayPoint2.linkedTo.Add(this); } } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) { return WayPointList.GetRandom(wp => wp.Submarine == sub && wp.ParentRuin == ruin && wp.spawnType == spawnType && - (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob.Prefab)) - , useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); + (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), + useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) @@ -775,7 +829,7 @@ namespace Barotrauma { if (assignedWayPoints[i] == null) { - DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!"); + DebugConsole.AddWarning("Couldn't find a waypoint for " + crew[i].Name + "!"); assignedWayPoints[i] = WayPointList[0]; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 9242f92cc..a899a5569 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -106,6 +106,7 @@ namespace Barotrauma class CharacterSpawnInfo : IEntitySpawnInfo { public readonly string identifier; + public readonly CharacterInfo CharacterInfo; public readonly Vector2 Position; public readonly Submarine Submarine; @@ -127,13 +128,17 @@ namespace Barotrauma this.onSpawned = onSpawn; } + public CharacterSpawnInfo(string identifier, Vector2 position, CharacterInfo characterInfo, Action onSpawn = null) : this (identifier, position, onSpawn) + { + CharacterInfo = characterInfo; + } public Entity Spawn() { var character = string.IsNullOrEmpty(identifier) ? null : Character.Create(identifier, Submarine == null ? Position : Submarine.Position + Position, - ToolBox.RandomSeed(8), createNetworkEvent: false); + ToolBox.RandomSeed(8), CharacterInfo, createNetworkEvent: false); return character; } @@ -302,6 +307,19 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } + public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (string.IsNullOrEmpty(speciesName)) + { + string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); + } + public void AddToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 33ef8bec8..f0b800508 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -16,10 +16,13 @@ namespace Barotrauma.Networking Control, UpdateSkills, Combine, + SetAttackTarget, ExecuteAttack, Upgrade, AssignCampaignInteraction, - ObjectiveManagerOrderState, + TeamChange, + ObjectiveManagerState, + AddToCrew, } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index d60b084ea..4894b2a32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -79,7 +79,6 @@ namespace Barotrauma.Networking TRAITOR_MESSAGE, MISSION, EVENTACTION, - RESET_UPGRADES, //inform the clients that the upgrades on the submarine have been reset CREW, //anything related to managing bots in multiplayer READY_CHECK //start, end and update a ready check } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 47233788a..b6eb1674f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Xna.Framework; +using System; namespace Barotrauma.Networking { @@ -40,24 +41,24 @@ namespace Barotrauma.Networking TargetEntity = targetEntity; } - private void WriteOrder(IWriteMessage msg) + public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, string orderOption, int orderPriority, int? wallSectionIndex) { - msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); + msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab)); + msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); + msg.Write(targetEntity is Entity ? (targetEntity as Entity).ID : (UInt16)0); // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously - if (Order.Prefab.Identifier != "dismissed") + if (order.Prefab.Identifier != "dismissed") { - msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); + msg.Write((byte)Array.IndexOf(order.Prefab.Options, orderOption)); } else { - if (!string.IsNullOrEmpty(OrderOption)) + if (!string.IsNullOrEmpty(orderOption)) { msg.Write(true); - string[] dismissedOrder = OrderOption.Split('.'); + string[] dismissedOrder = orderOption.Split('.'); msg.Write((byte)dismissedOrder.Length); if (dismissedOrder.Length > 0) { @@ -79,9 +80,9 @@ namespace Barotrauma.Networking } } - msg.Write((byte)OrderPriority); - msg.Write((byte)Order.TargetType); - if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) + msg.Write((byte)orderPriority); + msg.Write((byte)order.TargetType); + if (order.TargetType == Order.OrderTargetType.Position && targetEntity is OrderTarget orderTarget) { msg.Write(true); msg.Write(orderTarget.Position.X); @@ -91,11 +92,117 @@ namespace Barotrauma.Networking else { msg.Write(false); - if (Order.TargetType == Order.OrderTargetType.WallSection) + if (order.TargetType == Order.OrderTargetType.WallSection) { - msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); + msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0)); } } } + + private void WriteOrder(IWriteMessage msg) + { + WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex); + } + + public struct OrderMessageInfo + { + public int OrderIndex { get; } + public Order OrderPrefab { get; } + public string OrderOption { get; } + public int? OrderOptionIndex { get; } + public Character TargetCharacter { get; } + public Order.OrderTargetType TargetType { get; } + public Entity TargetEntity { get; } + public OrderTarget TargetPosition { get; } + public int? WallSectionIndex { get; } + public int Priority { get; } + + public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority) + { + OrderIndex = orderIndex; + OrderPrefab = orderPrefab; + OrderOption = orderOption; + OrderOptionIndex = orderOptionIndex; + TargetCharacter = targetCharacter; + TargetType = targetType; + TargetEntity = targetEntity; + TargetPosition = targetPosition; + WallSectionIndex = wallSectionIndex; + Priority = orderPriority; + } + } + + public static OrderMessageInfo ReadOrder(IReadMessage msg) + { + int orderIndex = msg.ReadByte(); + ushort targetCharacterId = msg.ReadUInt16(); + Character targetCharacter = targetCharacterId != Entity.NullEntityID ? Entity.FindEntityByID(targetCharacterId) as Character : null; + ushort targetEntityId = msg.ReadUInt16(); + Entity targetEntity = targetEntityId != Entity.NullEntityID ? Entity.FindEntityByID(targetEntityId) : null; + + Order orderPrefab = null; + int? optionIndex = null; + string orderOption = null; + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + { + orderPrefab = Order.PrefabList[orderIndex]; + if (orderPrefab.Identifier != "dismissed") + { + optionIndex = msg.ReadByte(); + } + // Does the dismiss order have a specified target? + else if (msg.ReadBoolean()) + { + int identifierCount = msg.ReadByte(); + if (identifierCount > 0) + { + int dismissedOrderIndex = msg.ReadByte(); + Order dismissedOrderPrefab = null; + if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + { + dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + orderOption = dismissedOrderPrefab.Identifier; + } + if (identifierCount > 1) + { + int dismissedOrderOptionIndex = msg.ReadByte(); + if (dismissedOrderPrefab != null) + { + var options = dismissedOrderPrefab.Options; + if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) + { + orderOption += $".{options[dismissedOrderOptionIndex]}"; + } + } + } + } + } + } + else + { + optionIndex = msg.ReadByte(); + } + + int orderPriority = msg.ReadByte(); + OrderTarget orderTargetPosition = null; + Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); + int wallSectionIndex = 0; + if (msg.ReadBoolean()) + { + float x = msg.ReadSingle(); + float y = msg.ReadSingle(); + ushort hullId = msg.ReadUInt16(); + var hull = hullId != Entity.NullEntityID ? Entity.FindEntityByID(hullId) as Hull : null; + orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); + } + else if (orderTargetType == Order.OrderTargetType.WallSection) + { + wallSectionIndex = msg.ReadByte(); + } + + return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index c5be46c81..a82fc6111 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -186,16 +186,12 @@ namespace Barotrauma.Networking if (updateReturnTimer > 1.0f) { updateReturnTimer = 0.0f; - - if (shuttleSteering != null) - { - shuttleSteering.SetDestinationLevelStart(); - } - UpdateReturningProjSpecific(); + shuttleSteering?.SetDestinationLevelStart(); + UpdateReturningProjSpecific(deltaTime); } } - partial void UpdateReturningProjSpecific(); + partial void UpdateReturningProjSpecific(float deltaTime); private IEnumerable ForceShuttleToPos(Vector2 position, float speed) { @@ -278,6 +274,7 @@ namespace Barotrauma.Networking if (hull.Submarine != RespawnShuttle) { continue; } hull.OxygenPercentage = 100.0f; hull.WaterVolume = 0.0f; + hull.BallastFlora?.Kill(); } foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 528f328bd..8b72e0075 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -635,6 +635,13 @@ namespace Barotrauma.Networking set; } + [Serialize(false, true)] + public bool AllowLinkingWifiToChat + { + get; + set; + } + [Serialize(true, true)] public bool AllowFriendlyFire { @@ -885,6 +892,8 @@ namespace Barotrauma.Networking get; set; } + // we do not serialize this value because it relies on a default setting + public int MaxMissionCount { get; set; } = CampaignSettings.DefaultMaxMissionCount; public void SetPassword(string password) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 86eababf0..059e082f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -35,13 +35,13 @@ namespace Barotrauma } /// - /// Returns the active prefab with identifier k. + /// Returns the active prefab with the identifier. /// - /// Prefab identifier - /// Active prefab with identifier k - public T this[string k] + /// Prefab identifier + /// Active prefab with the identifier + public T this[string identifier] { - get { return prefabs[k].Last(); } + get { return prefabs[identifier].Last(); } } /// @@ -63,13 +63,13 @@ namespace Barotrauma } /// - /// Returns true if a prefab with identifier k exists, false otherwise. + /// Returns true if a prefab with the identifier exists, false otherwise. /// - /// Prefab identifier - /// Whether a prefab with identifier k exists or not - public bool ContainsKey(string k) + /// Prefab identifier + /// Whether a prefab with the identifier exists or not + public bool ContainsKey(string identifier) { - return prefabs.ContainsKey(k); + return prefabs.ContainsKey(identifier); } /// @@ -93,11 +93,10 @@ namespace Barotrauma //Handle bad overrides and duplicates if (basePrefabExists && !isOverride) { - DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Failed to add the prefab \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T)}): a prefab with the same identifier already exists; try overriding\n{Environment.StackTrace}"); return; } - //Add to list if (!basePrefabExists) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index e7c3e5da5..8cd8dae34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -53,7 +53,41 @@ namespace Barotrauma return GameMain.Server.ServerSettings.RadiationEnabled; #endif } - + + public void SetMaxMissionCount(int maxMissionCount) + { +#if SERVER + if (GameMain.Server != null) + { + if (maxMissionCount < CampaignSettings.MinMissionCountLimit) maxMissionCount = CampaignSettings.MaxMissionCountLimit; + if (maxMissionCount > CampaignSettings.MaxMissionCountLimit) maxMissionCount = CampaignSettings.MinMissionCountLimit; + + GameMain.Server.ServerSettings.MaxMissionCount = maxMissionCount; + lastUpdateID++; + } +#endif +#if CLIENT + (maxMissionCountText as GUITextBlock).Text = maxMissionCount.ToString(); +#endif + } + + public int GetMaxMissionCount() + { +#if CLIENT + // this seems rather silly, but it matches the radiation enabled check structurally. is this right? + if (maxMissionCountText != null && Int32.TryParse(maxMissionCountText.Text, out int result)) + { + return result; + } + else + { + return 0; + } +#elif SERVER + return GameMain.Server.ServerSettings.MaxMissionCount; +#endif + } + public void ToggleTraitorsEnabled(int dir) { #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 4f87592ca..f04d1fc1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Reflection; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -32,7 +33,7 @@ namespace Barotrauma public string FallBackTextTag; /// - /// Currently implemented only for int fields. TODO: implement the remaining types (SerializableEntityEditor) + /// Currently implemented only for int and bool fields. TODO: implement the remaining types (SerializableEntityEditor) /// public bool ReadOnly; @@ -60,6 +61,37 @@ namespace Barotrauma { } + [AttributeUsage(AttributeTargets.Property)] + class ConditionallyEditable : Editable + { + public ConditionallyEditable(ConditionType conditionType) + { + this.conditionType = conditionType; + } + + private readonly ConditionType conditionType; + + public enum ConditionType + { + //These need to exist at compile time, so it is a little awkward + //I would love to see a better way to do this + AllowLinkingWifiToChat, + IsSwappableItem + } + + public bool IsEditable(ISerializableEntity entity) + { + switch (conditionType) + { + case ConditionType.AllowLinkingWifiToChat: + return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; + case ConditionType.IsSwappableItem: + return entity is Item item && item.Prefab.SwappableItem != null; + } + return false; + } + } + [AttributeUsage(AttributeTargets.Property)] public class Serialize : Attribute @@ -618,7 +650,7 @@ namespace Barotrauma return dictionary; } - public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false) + public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false, bool ignoreEditable = false) { var saveProperties = GetProperties(obj); foreach (var property in saveProperties) @@ -635,7 +667,7 @@ namespace Barotrauma foreach (var attribute in property.Attributes.OfType()) { if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) || - property.Attributes.OfType().Any()) + (!ignoreEditable && property.Attributes.OfType().Any())) { save = true; break; @@ -737,6 +769,10 @@ namespace Barotrauma if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, entity, attribute); + if (property.Name == nameof(ItemComponent.Msg) && entity is ItemComponent component) + { + component.ParseMsg(); + } } else if (entity is Item item1) { @@ -745,12 +781,16 @@ namespace Barotrauma if (component.SerializableProperties.TryGetValue(attributeName, out SerializableProperty componentProperty)) { FixValue(componentProperty, component, attribute); + if (componentProperty.Name == nameof(ItemComponent.Msg)) + { + ((ItemComponent)component).ParseMsg(); + } } } } } - void FixValue(SerializableProperty property, object parentObject, XAttribute attribute) + static void FixValue(SerializableProperty property, object parentObject, XAttribute attribute) { if (attribute.Value.Length > 0 && attribute.Value[0] == '*') { @@ -773,6 +813,29 @@ namespace Barotrauma property.TrySetValue(parentObject, ((Point)property.GetValue(parentObject)).Multiply(multiplier)); } } + else if (attribute.Value.Length > 0 && attribute.Value[0] == '+') + { + if (property.PropertyType == typeof(int)) + { + float.TryParse(attribute.Value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out float addition); + property.TrySetValue(parentObject, (int)(((int)property.GetValue(parentObject)) + addition)); + } + else if (property.PropertyType == typeof(float)) + { + float.TryParse(attribute.Value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out float addition); + property.TrySetValue(parentObject, (float)property.GetValue(parentObject) + addition); + } + else if (property.PropertyType == typeof(Vector2)) + { + var addition = XMLExtensions.ParseVector2(attribute.Value.Substring(1)); + property.TrySetValue(parentObject, (Vector2)property.GetValue(parentObject) + addition); + } + else if (property.PropertyType == typeof(Point)) + { + var addition = XMLExtensions.ParsePoint(attribute.Value.Substring(1)); + property.TrySetValue(parentObject, ((Point)property.GetValue(parentObject)) + addition); + } + } else { property.TrySetValue(parentObject, attribute.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 5ff8bbccd..6c0eccc0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -28,7 +28,7 @@ namespace Barotrauma return spr; } return null; - }).Where(s => s!=null).ToList(); + }).Where(s => s != null).ToList(); } return retVal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 8090206e6..01bb1451e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -668,9 +668,10 @@ namespace Barotrauma return true; } - public void GetNearbyTargets(Vector2 worldPosition, List targets) + public IEnumerable GetNearbyTargets(Vector2 worldPosition, List targets = null) { - if (Range <= 0.0f) { return; } + targets ??= new List(); + if (Range <= 0.0f) { return targets; } if (HasTargetType(TargetType.NearbyCharacters)) { foreach (Character c in Character.CharacterList) @@ -707,6 +708,7 @@ namespace Barotrauma } } } + return targets; bool CheckDistance(ISpatialEntity e) { @@ -746,7 +748,18 @@ namespace Barotrauma { owner = ownerItem.ParentInventory?.Owner; } - if (owner is Item container && HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) + { + //if we're checking for tags, just check the Item object, not the ItemComponents + if (HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + } + else + { + if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + } + } if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } } else @@ -1235,6 +1248,12 @@ namespace Barotrauma Projectile projectile = newItem.GetComponent(); if (projectile != null && user != null && sourceBody != null && entity != null) { + var rope = newItem.GetComponent(); + if (rope != null && sourceBody.UserData is Limb sourceLimb) + { + rope.Attach(sourceLimb, newItem); + } + float spread = MathHelper.ToRadians(Rand.Range(-itemSpawnInfo.AimSpread, itemSpawnInfo.AimSpread)); var worldPos = sourceBody.Position; float rotation = itemSpawnInfo.Rotation; diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index c0c645e9b..24c3db01d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -37,6 +37,10 @@ namespace Barotrauma private static RoundData roundData; + // Used for the Extravehicular Activity ("crewaway") achievement + private static PathFinder pathFinder; + private static readonly Dictionary cachedDistances = new Dictionary(); + public static void OnStartRound() { roundData = new RoundData(); @@ -45,6 +49,8 @@ namespace Barotrauma Reactor reactor = item.GetComponent(); if (reactor != null) { roundData.Reactors.Add(reactor); } } + pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + cachedDistances.Clear(); } public static void Update(float deltaTime) @@ -153,12 +159,44 @@ namespace Barotrauma if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { - float dist = 500 / Physics.DisplayToRealWorldRatio; - if (Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition) > - dist * dist) + float requiredDist = 500 / Physics.DisplayToRealWorldRatio; + float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition); + if (cachedDistances.TryGetValue(c, out var cachedDistance)) + { + if (cachedDistance.ShouldUpdateDistance(c.WorldPosition, Submarine.MainSub.WorldPosition)) + { + cachedDistances.Remove(c); + cachedDistance = CalculateNewCachedDistance(c); + if (cachedDistance != null) + { + cachedDistances.Add(c, cachedDistance); + } + } + } + else + { + cachedDistance = CalculateNewCachedDistance(c); + if (cachedDistance != null) + { + cachedDistances.Add(c, cachedDistance); + } + } + if (cachedDistance != null) + { + distSquared = Math.Max(distSquared, cachedDistance.Distance * cachedDistance.Distance); + } + if (distSquared > requiredDist * requiredDist) { UnlockAchievement(c, "crewaway"); } + + static CachedDistance CalculateNewCachedDistance(Character c) + { + pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(c.WorldPosition), ConvertUnits.ToSimUnits(Submarine.MainSub.WorldPosition)); + if (path.Unreachable) { return null; } + return new CachedDistance(c.WorldPosition, Submarine.MainSub.WorldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 65891b222..1881729df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -46,6 +46,8 @@ namespace Barotrauma { var value = (float) OriginalValue; + if (level == 0) { return value; } + if (Multiplier[^1] != '%') { float multiplier = ParseValue(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index dc0173d3e..06f4c29fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -46,7 +46,7 @@ namespace Barotrauma int price = BasePrice; for (int i = 1; i <= level; i++) { - price += (int)(price * MathHelper.Lerp( IncreaseLow, IncreaseHigh, i / (float)Prefab.MaxLevel) / 100); + price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, i / (float)Prefab.MaxLevel) / 100); } return location?.GetAdjustedMechanicalCost(price) ?? price; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/CrossThread.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs similarity index 69% rename from Barotrauma/BarotraumaClient/ClientSource/Utils/CrossThread.cs rename to Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs index bf5eb95c1..c1225eb7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/CrossThread.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CrossThread.cs @@ -59,5 +59,25 @@ namespace Barotrauma newTask.PerformWait(); } } + + public static void AddOnMainThread(this List list, T element) + { + RequestExecutionOnMainThread(() => { list.Add(element); }); + } + + public static void AddRangeOnMainThread(this List list, IEnumerable elements) + { + RequestExecutionOnMainThread(() => { list.AddRange(elements); }); + } + + public static void RemoveOnMainThread(this List list, T element) + { + RequestExecutionOnMainThread(() => { list.Remove(element); }); + } + + public static void ClearOnMainThread(this List list) + { + RequestExecutionOnMainThread(() => { list.Clear(); }); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs new file mode 100644 index 000000000..76897c56d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Either.cs @@ -0,0 +1,62 @@ +using System; + +namespace Barotrauma +{ + public abstract class Either + { + public static implicit operator Either(T t) => new EitherT(t); + public static implicit operator Either(U u) => new EitherU(u); + + public static explicit operator T(Either e) => e.TryGet(out T t) ? t : throw new InvalidCastException($"Contained object is not of type {typeof(T).Name}"); + public static explicit operator U(Either e) => e.TryGet(out U u) ? u : throw new InvalidCastException($"Contained object is not of type {typeof(U).Name}"); + + public abstract bool TryGet(out T t); + public abstract bool TryGet(out U u); + + public abstract bool TryCast(out V v); + + public abstract override string ToString(); + } + + public sealed class EitherT : Either + { + public readonly T Value; + + public EitherT(T value) { Value = value; } + + public override string ToString() + { + return Value.ToString(); + } + + public override bool TryGet(out T t) { t = Value; return true; } + public override bool TryGet(out U u) { u = default(U); return false; } + + public override bool TryCast(out V v) + { + if (Value is V result) { v = result; return true; } + else { v = default(V); return false; } + } + } + + public sealed class EitherU : Either + { + public readonly U Value; + + public EitherU(U value) { Value = value; } + + public override string ToString() + { + return Value.ToString(); + } + + public override bool TryGet(out T t) { t = default(T); return false; } + public override bool TryGet(out U u) { u = Value; return true; } + + public override bool TryCast(out V v) + { + if (Value is V result) { v = result; return true; } + else { v = default(V); return false; } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index fd7cb014d..2493257e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -26,6 +26,7 @@ namespace Barotrauma sanitizedText = text; if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator)) { + text = text.Replace("\r", ""); string[] segments = text.Split(definitionIndicator); sanitizedText = string.Empty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 1d68cccc5..fe65006e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -391,7 +391,7 @@ namespace Barotrauma } private static Dictionary> cachedLines = new Dictionary>(); - public static string GetRandomLine(string filePath) + public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.Server) { List lines; if (cachedLines.ContainsKey(filePath)) @@ -418,7 +418,7 @@ namespace Barotrauma } if (lines.Count == 0) return ""; - return lines[Rand.Range(0, lines.Count, Rand.RandSync.Server)]; + return lines[Rand.Range(0, lines.Count, randSync)]; } /// diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 38d4e7f67..8cd68e513 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index b2208771e..3e0c87dbc 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 8c1105f88..0d93cd70f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index 3c5994800..9f6a1c043 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 7a5dc04a7..642369fbc 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 5e14feb89..a3aba369b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 28b1c9410..648425a0b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index a8cd9ff9f..10ea9b77e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index be89e7cf0..a9c57b323 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index c70b638d3..a58fee96f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index 943887519..aea6bb39b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index b0fb3144b..6b9f2045f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 4518bcd0f..6eaf45b30 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 9b74542ee..2bfab309f 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,184 @@ ------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------- +v0.14.6.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "entity not found" errors when receiving attack data for a character that's been removed (most often a terminal cell). +- Fixed occasional "mission equality check failed" errors when starting a round with multiple missions selected. +- Fixed outpost events not triggering in levels with a difficulty between 40-41. +- Fixed hunting grounds missions only spawning endworms. +- Fixed 2nd part of the Jacov Subra event chain not triggering. + +--------------------------------------------------------------------------------------------------------- +v0.14.5.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed deconstructing chaingun/laser ammo boxes not giving back aluminium. +- Fixes certain particles (e.g. flare) "dragging behind" inside moving subs. +- Fixed Typhon 2's railgun not being connected to the loader. +- Fixed occasional "failed to read event for xxx" errors when syncing a pirate's AI state in multiplayer. +- Fixed atan components working unreliably when using the separate x/y inputs. +- Fixed missing prisoner outfit sprite. +- Crates pre-placed into a submarine's cargo containers or ULD's aren't taken into account when calculating the cargo capacity to display in the submarine preview. +- Fixed limbs getting automatically deselected in the health interface when receiving a new affliction (most noticeable when trying to heal an affliction that's being continuously applied on the character, e.g. burns caused by radiation). +- Added a missing hull to beacon station's airlock room. +- Fixed inventory toggle button being rendered in the top-left corner of the screen when grabbing a character with a 0-capacity inventory (such as a hatchling). +- Fixed an exploit that allowed crashing the game using certain looping circuits that involve trigonometric function components. +- Removed underwater explosion particle effect when vomiting underwater. +- Fixed screen noise effect cutting from white to black with no blending when the oxygen low affliction kicks in while the white noise effect (e.g. radiation sickness) is active. +- Fixed ability to set the reactor's turbine output or fission rate outside the allowed range of 0-100 using the "set_fissionrate" and "set_turbineoutput" inputs. +- Fixed crashing when opening the weapon customization menu when there's non-swappable turrets in the sub. +- Fixed misaligned light sprites on nav terminal and status monitor. + +Modding: +- Fixed crashing when trying to create a thalamus when there's no wreck AI configs available in the selected content packages. + +--------------------------------------------------------------------------------------------------------- +v0.14.4.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Submarine weapons can be swapped in the outposts. +- Added Pulse Laser and Chaingun. +- Added Canister Shells (a reloadable burst-type munition for the railgun). +- Added escort missions. +- Added pirate missions. +- Option to choose multiple missions per level. +- Made it possible to sell items on the sub through a new store tab in singleplayer. +- Cargo now spawns in crate shelves and "Unit Load Devices" (large containers mostly used in cargo subs). The amount of cargo you can transport depends on the number of the shelves/ULDs, and the cargo mission rewards scale according to the cargo capacity. If there's no shelves or ULDs, you can still transport a small number of crates on the cargo room's floor like before. +- Made upgrades affect all submarines. Old purchased submarines will have their upgrades overridden by the currently loaded submarine's upgrades. +- The previous music track continues playing when the game switches back from the "intensity tracks" to normal music. +- Made it possible to carry diving suits and toolbelts in hands. We could use some feedback on the way this works: is it intuitive, should there be a separate key for picking them up...? +- The ice shards and exploding mushrooms in caves can be destroyed with weapons and explosives. +- Added tracer particles to raycast projectiles (hitscan). +- Increased sonar beacon range. +- Color character names according to the team when using the Health Scanner HUD. +- Allow combining elastin. +- Character orders and ignore orders now persist between rounds and saved sessions. +- Added indicators for the tasks the bots are currently doing when they are not following the orders. +- Revisited the random (monster) events: more Watchers and Tigerthreshers, less Crawlers. Fixed a number of issues. Feedback appreciated. (WIP) +- Numerous fixes and improvements on particle effects. +- Changed how autonomous steering objective works. Captains now idle near the helm instead of standing next to the nav terminal. +- Only show the "Save and Quit" button when the game can be saved. Renamed the button in the lobby to "Quit". Saving in multiplayer is done like in single player: on level transitions and on exit from an outpost. +- Updated sounds for Endworm and the electrical discharger. +- Increased the stun of smg rounds from 0.125 to 0.15 to give it a bit more stopping power. +- Lowered Husks' health regeneration and bleeding reduction. Crawler Husks now regenerate too. Lowered their health a bit to compensate it. +- Hammerhead and Golden Hammerhead: Increased health, added some protection on claws and the tail end. Slightly increase the slow swimming speed and animations. The claws don't break anymore when shot with coilgun. +- Hammerheads and Husks don't avoid gun fire anymore. Pets now avoid gun fire. +- Removed Endworm's weak point in the mouth. +- Most outpost events no longer trigger automatically, but require interacting with a specific item/character. +- Taking items that contain stolen items counts as stealing, so you can't for example put a toolbox inside an outpost cabinet, load it full of items and then take it. +- Added a button for resetting in-game hints to the settings. +- Mention diving suits' depth limits in their descriptions. +- Modified the locked path tooltips to mention the officer in the outpost. +- Reset reactor fission rate, turbine output and temperature to optimal levels at the start of a round. Prevents the reactor from catching fire at the start of a round if it was being operated at a high fission rate at the end of the previous round. +- Removed ability to drag and drop seeds into planters. +- Added ability to load .sub, .xml, .png and .jpeg files in sub editor by dragging and dropping them onto the game window. +- Added new biome-specific background noise tracks. +- Improved the command interface minimap view by adding connector lines between icons and item positions to better visualize which item each icon is linked to. +- Projectiles go through severed limbs. +- Docking ports can't be rewired in outpost levels because it can be abused to force to submarine to depart from the outpost. +- Allow clicking on the main menu buttons when the credits are open. +- Option to adjust motion sensor's update interval in the sub editor. + +Fixes: +- Fixed clients timing out at the start of a multiplayer round if loading the campaign save takes more than 30 seconds. +- Fixed previous missions still being available in outposts that have become abandoned due to radiation. +- Fixed eggs sometimes spawning partially inside walls in abandoned outpost missions. +- Fixed thalamus walls' colliders still being present even if the wreck doesn't contain a thalamus, preventing entering the sub from places where thalamus walls used to be. +- Fixed minerals sometimes not being mineable in outposts. +- Fixed small characters being unable to avoid the edges of the hulls when idling. +- Save submarine and characters' inventories when saving and quitting an mp campaign round in an outpost. +- Marked "control" console command as a cheat. +- Fixed items sometimes disappearing client-side when the server corrects their position from outside to inside. +- The traitor mission to sabotage the engine doesn't require sabotaging shuttle engines. +- Stacks of seeds can't be put into planters. +- Fixed non-repeatable events (e.g. mission events) no longer appearing after completing the campaign. +- Fixed security officers using head set batteries when their stun batons are out of batteries, causing them to be unable to hear or receive orders (#5681). +- Fixed security officers often handcuffing you after one hit with stun baton (#5649). +- Fixed ignore orders not being properly synced when joining a multiplayer game mid-round. +- Fixed abyss monsters not spawning if there's any player sub (including the respawn shuttle) above the abyss. +- Fixed "are you sure you want to depart without a mission" prompt popping up even if there's no missions available. +- Fixed health bars showing up through walls. +- Fixed horizontal docking ports sometimes docking from the wrong side in mirrored subs. +- Fixed mouse wheel zooming the nav terminal view when the server log (or any other UI element) is blocking it. +- Fixed campaign's initial text popup getting stuck if the sub automatically undocks at the start of a round. +- Non-empty items (ammo boxes, fuel rods, etc) can't be recycled. +- Made airlock door assembly behave a bit more reliably. The circuit in the assembly simply toggles the state of both doors, meaning that one of the doors needs to always be closed and the other open for the logic to work correctly. If using the assembly in a respawn shuttle, it'd break when the shuttle leaves and it's doors are forced to close. +- Fixed inability to go through docking ports when the door/hatch at the other side is broken. +- Fixed other entities' sprites disappearing when reloading a sprite in the sub editor. +- Fixed Remora drone's docking hatch being repairable with a welding tool instead of a wrench. +- Fixed some of Remora drone's walls being transparent. +- Fixed rotation of mirrored items getting messed up when saving and reloading a sub. +- Fixed inability to detach items attached outside the sub. +- Fixed items that are inside a hull being difficult to target from outside the sub (docking ports/hatches in particular). +- Fixed crashing when trying to load a campaign save that contains pets that can't be found (e.g. if you've saved while using a mod that adds custom pets and try to load the save in the vanilla game). +- Fixed crashing when trying to change a location's type to a type that can't be found (e.g. if a mod includes custom location types which are configured incorrectly). +- Fixed "Ignore This" order affecting all NPCs. It should only affect the bots in a player team. +- Fixed "Ignore This" order restricting player access to doors. +- Fixed bots targeting same targets when fixing/repairing. +- Fixed monsters sometimes getting stuck "dancing" near the submarine because they try to avoid and target it at the same time. +- Monster events: Fixed many higher difficulty subsets spawning multiple monster events when they should spawn only one. +- Fixed items taken from abandoned outposts sometimes reappearing when returning to the outpost. +- Fixed highlighting items sometimes breaking after dropping a metal crate or some other container whose inventory is always visible when the item is equipped. +- Fixed bots putting minerals in the closest locker instead of preferring those that have been tagged with the "mineralcontainer" tag +- Fixed bots not respecting the access restrictions when choosing a where to put items into. +- Fixed bots being able to take items from secure cabinets if the item is inside another container inside the cabinet. +- Fixes to hull generation between docking ports. Should fix tiny gaps between the hulls that caused characters to briefly teleport outside the sub when passing through the port, and hulls generating incorrectly when the main submarine has hulls at both sides of the docking port. +- Fixed captain's pipe, harmonica and pipe tobacco not spawning in crates when purchased. +- Fixed console errors when trying to overwrite an existing item assembly in the sub editor. +- Fixed the margin calculations that in some cases made it impossible for Bonethreshers (and possibly other monsters) to reach a moving target. +- Fixed abyss monsters sometimes being unable to hit the sub and just keep pushing the sub. +- Define "tool", "weapon", and "provocative" targeting params to be ignored if the creature is not in the same sub as the target. Fixes some odd cases where the monsters e.g. target some item inside the sub when they are outside. +- Fixed occasional excessive camera shake when a large monster is lodged between the sub and the level. +- Fixed ballast flora not getting cleared from respawn shuttles when the shuttle despawns. +- Fixed ability to switch back to a sub you've left behind using the outpost terminals. +- Fixed doors without integrated buttons getting instantly opened by clicking on them when you have access to the button. +- Fixed ability to switch control to the hostages with Z and X keys during hostage missions. +- Fixed abyss and combat suits not getting autofilled with oxygen tanks when placing the initial supplies to a sub. +- Fixed inability to type in the "max players" field in the "host server" tab. +- Fixed item sell quantity sometimes appearing "maxed out" at a quantity less than what the player is actually selling. +- Fixed crashing when the team id argument for the "spawn" console command is formatted incorrectly, mention the argument in the command's help text. +- Fixed bots trying to clean up seeds and put items into bags that were moved to somewhere the bot can't access them, causing the bot to be stuck trying to reach the target. +- Fixed missions sometimes unlocking in paths leading to an irradiated location even if there's a more suitable path available. +- Fixed white rectangle around selected items in the sub editor being slightly off if the item's position/size is not a whole number. +- Fixed "incorrect password" text overlapping with the buttons in the password prompt. +- Fixed z-fighting in Medium Weapons Display Case. +- Fixed inability to drop through broken hatches. +- Fixed sprite bleed in IC-4 block's inventory icon. +- Fixed healthbar and affliction area being clickable even if there's an UI element in front of them, and even if there's no afflictions in the affliction area. Most noticeable when editing an electrical components properties. +- Fixed some incorrect room names in the beacon stations. +- Fixed propeller damage area triggering very inaccurately, particularly on shuttle engines. +- Fixed hull indicators fading in when loading a sub in the editor (they should only get hidden and fade in when editing the ambient light value). +- Fixed gaps sometimes getting linked incorrectly to the hulls between docking hatches, preventing water from flowing down from the lower hull in mirrored subs. Happened in mirrored Kastrull for example. +- Fixed fires in hulls between docking ports never going out client-side. +- Fixed nav terminal's, sonar monitor's and status monitor's selection rectangles being offset from the sprite in the sub editor. +- Fixed mirrored pumps emitting particles from the wrong side of the pump. + +Modding: +- Made it possible to use StatusHUD components in non-equippable items like turrets. +- Fixed scripted event's StatusEffectAction not being able to target ItemComponents. +- Added support for making missions trigger scripted events. +- Option to configure the color of an explosion's flash. +- Option to configure the number of gates between biomes (see MapGenerationParameters.xml). +- Turret's barrel and rail sprites are shown in the sprite editor. +- Made SecondaryUse status effects work with melee and ranged weapons. +- Renamed "Inflitrate" to "CanOpenDoors" and rewrote the tooltip. +- Added "scalemultiplier" (Vector2) for particle emitters so that the effects can be scaled just in one axis instead of both. +- Fixed overriding nav terminals with a mod resetting the changes to the custom interface buttons. +- Fixed status effect conditionals that check a parent container's tags checking the container's components, not just the item itself. Resulted in tag inequality checks always succeeding, because the components don't have any tags. +- Fixed crashing if a scripted event tries to spawn a human prefab that can't be found (i.e. if a mod uses a non-existent human prefab identifier). +- RemoveItemAction can be used to remove tagged items directly, not just items in a tagged character's inventory. +- Replaced "spawnprobability" attribute in monster events with a generic "probability" attribute that can be used for any type of event. +- Option to spawn particles across the ray cast from a hitscan projectile (see "pulselaserbolt"). +- Fixed linked subs included in outpost modules being positioned incorrectly. +- Added "swapidentifier" to SwappableItem definitions. Can be used to restrict which items in a given category can be swapped with each other (e.g. if you want to add custom upgradeable turrets but not make them swappable with the vanilla turrets). +- Made SecondaryUse status effects work with ranged weapons. +- Added "equal" targeting tag to enemy AI. Makes it possible to define how monsters react to monsters with the same combat strength. +- TagAction can be used to tag characters based on the human prefab identifier. +- Fixed a crash in the character editor that happened when a humanoid character had an elbow joint but didn't (yet) have a wrist joint. +- The "avoid gun fire" parameter now defaults to false. + +--------------------------------------------------------------------------------------------------------- v0.13.3.11 --------------------------------------------------------------------------------------------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed0ef8d52..a40f994b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,14 +3,11 @@ Welcome to Barotrauma's GitHub repository! If you're here to report an issue or to contribute to the development, please read the instructions below before you do. ## I have a question! -Please check [our FAQ](https://barotraumagame.com/faq/) in case the question has already been answered. If not, you should stop by at our [Discord Server](discord.gg/undertow) and [Steam forums](https://steamcommunity.com/app/602960/discussions/). +Please check [our FAQ](https://barotraumagame.com/faq/) in case the question has already been answered. If not, you can post the question on the [Barotrauma discussion forum](https://undertowgames.com/forum/viewforum.php?f=17) or stop by at our [Discord Server](https://discord.gg/undertow). ## Reporting a bug If you've encountered a bug, you can report it in the [issue tracker](https://github.com/Regalis11/Barotrauma/issues). Please follow the instructions in the issue template to make it easier for us to diagnose and fix the issue. -## Sharing ideas and suggestions -To keep the number of tickets in the issue tracker manageable, please do not post feature requests as tickets. Instead, you can use the [Discussions section](https://github.com/Regalis11/Barotrauma/discussions) to share and discuss feedback, ideas and suggestions for future development. - ## Code contributions Before you start doing modifications to the code or submitting pull requests to the repository, it is important that you've read and understood (at least the human-readable summary part) of [our EULA](https://github.com/Regalis11/Barotrauma/blob/master/EULA.txt). To sum it up, Barotrauma is not an open source project in the sense of free, open source software that you can freely distribute or reuse. Even though the early versions of the game have been available for free, current versions of the game have a price tag. If you're not comfortable with your contributions potentially being used in a commercial product, do not submit pull requests to the repository. diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/DelaunayTriangle.cs b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/DelaunayTriangle.cs index 2eda5b482..4352e34c7 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/DelaunayTriangle.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/DelaunayTriangle.cs @@ -211,10 +211,7 @@ namespace FarseerPhysics.Common.Decomposition.CDT.Delaunay for (int i = 0; i < 3; i++) { t = Neighbors[i]; - if (t != null) - { - t.ClearNeighbor(this); - } + t?.ClearNeighbor(this); } ClearNeighbors(); Points[0] = Points[1] = Points[2] = null; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/Sweep/DTSweep.cs b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/Sweep/DTSweep.cs index df5dacf35..ef50f2a88 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/Sweep/DTSweep.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Delaunay/Sweep/DTSweep.cs @@ -491,10 +491,7 @@ namespace FarseerPhysics.Common.Decomposition.CDT.Delaunay.Sweep { triangle.MarkConstrainedEdge(index); triangle = triangle.Neighbors[index]; - if (triangle != null) - { - triangle.MarkConstrainedEdge(ep, eq); - } + triangle?.MarkConstrainedEdge(ep, eq); return true; } return false; diff --git a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Polygon/Polygon.cs b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Polygon/Polygon.cs index f39fe5be3..0bf8aa999 100644 --- a/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Polygon/Polygon.cs +++ b/Libraries/Farseer Physics Engine 3.5/Common/Decomposition/CDT/Polygon/Polygon.cs @@ -187,10 +187,7 @@ namespace FarseerPhysics.Common.Decomposition.CDT.Polygon public void ClearSteinerPoints() { - if (_steinerPoints != null) - { - _steinerPoints.Clear(); - } + _steinerPoints?.Clear(); } /// diff --git a/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Store/GAStore.cs b/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Store/GAStore.cs index b252ab126..2575cd43a 100644 --- a/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Store/GAStore.cs +++ b/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Store/GAStore.cs @@ -201,15 +201,9 @@ namespace GameAnalyticsSDK.Net.Store } finally { - if(command != null) - { - command.Dispose(); - } + command?.Dispose(); - if(transaction != null) - { - transaction.Dispose(); - } + transaction?.Dispose(); } // Return results diff --git a/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Utilities/SimpleJSON.cs b/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Utilities/SimpleJSON.cs index 160760eb7..9f4faaad3 100644 --- a/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Utilities/SimpleJSON.cs +++ b/Libraries/GameAnalytics/GA-SDK-MONO-SHARED/Utilities/SimpleJSON.cs @@ -595,10 +595,7 @@ namespace GameAnalyticsSDK.Net.Utilities break; } stack.Push(new JSONObject()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } + ctx?.Add(TokenName, stack.Peek()); TokenName = ""; Token.Length = 0; ctx = stack.Peek(); @@ -612,10 +609,7 @@ namespace GameAnalyticsSDK.Net.Utilities } stack.Push(new JSONArray()); - if (ctx != null) - { - ctx.Add(TokenName, stack.Peek()); - } + ctx?.Add(TokenName, stack.Peek()); TokenName = ""; Token.Length = 0; ctx = stack.Peek(); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/FileDropEventArgs.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/FileDropEventArgs.cs new file mode 100644 index 000000000..21382f965 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/FileDropEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.Xna.Framework +{ + public class FileDropEventArgs : EventArgs + { + public string FilePath; + public FileDropEventArgs(string filePath) + { + FilePath = filePath; + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs index 2ef56f348..ec4d56407 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/GameWindow.cs @@ -88,6 +88,7 @@ namespace Microsoft.Xna.Framework { #region Events + public event EventHandler FileDropped; public event EventHandler ClientSizeChanged; public event EventHandler OrientationChanged; public event EventHandler ScreenDeviceNameChanged; @@ -131,7 +132,7 @@ namespace Microsoft.Xna.Framework { protected void OnDeactivated () { } - + protected void OnOrientationChanged () { EventHelpers.Raise(this, OrientationChanged, EventArgs.Empty); @@ -153,7 +154,12 @@ namespace Microsoft.Xna.Framework { } #endif - protected internal abstract void SetSupportedOrientations (DisplayOrientation orientations); + protected internal abstract void SetSupportedOrientations (DisplayOrientation orientations); protected abstract void SetTitle (string title); + + protected void OnFileDropped(FileDropEventArgs e) + { + EventHelpers.Raise(this, FileDropped, e); + } } } 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 7ef537fd0..5f9dbc68a 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 @@ -36,6 +36,7 @@ + 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 e32bf3898..dd5a32d33 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 @@ -36,6 +36,7 @@ + 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 a98cc5944..3ff6019b1 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 @@ -36,6 +36,7 @@ + diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 5dc1e0ea4..0da7ec4c5 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -146,6 +146,8 @@ internal static class Sdl public Joystick.DeviceEvent JoystickDevice; [FieldOffset(0)] public GameController.DeviceEvent ControllerDevice; + [FieldOffset(0)] + public DropEvent DropEvent; } public struct Rectangle @@ -172,6 +174,10 @@ internal static class Sdl GetError(SDL_Init(flags)); } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void d_sdl_free(IntPtr ptr); + public static d_sdl_free Free = FuncLoader.LoadFunction(NativeLibrary, "SDL_free"); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void d_sdl_disablescreensaver(); public static d_sdl_disablescreensaver DisableScreenSaver = FuncLoader.LoadFunction(NativeLibrary, "SDL_DisableScreenSaver"); @@ -310,6 +316,15 @@ internal static class Sdl public delegate int d_sdl_sethint(string name, string value); public static d_sdl_sethint SetHint = FuncLoader.LoadFunction(NativeLibrary, "SDL_SetHint"); + [StructLayout(LayoutKind.Sequential)] + public struct DropEvent + { + public EventType Type; + public uint Timestamp; + public IntPtr File; + public uint WndowID; + } + public static class Window { public const int PosUndefined = 0x1FFF0000; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs index cc9f42204..e18d8a4d4 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGamePlatform.cs @@ -126,6 +126,12 @@ namespace Microsoft.Xna.Framework Window.MouseState.X = ev.Motion.X; Window.MouseState.Y = ev.Motion.Y; } + else if (ev.Type == Sdl.EventType.DropFile) + { + string file = InteropHelpers.Utf8ToString(ev.DropEvent.File); + Sdl.Free(ev.DropEvent.File); //required according to SDL's documentation + _view.DropFile(file); + } else if (ev.Type == Sdl.EventType.KeyDown) { var key = KeyboardUtil.ToXna(ev.Key.Keysym.Sym); @@ -135,7 +141,7 @@ namespace Microsoft.Xna.Framework //TODO: rethink all of this char character = (char)KeyboardUtil.ApplyModifiers(ev.Key.Keysym.Sym, ev.Key.Keysym.Mod); - + if ((int)((char)ev.Key.Keysym.Sym) != ev.Key.Keysym.Sym) { character = '\0'; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs index 1fc2d8810..7d5a071ca 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDLGameWindow.cs @@ -286,7 +286,7 @@ namespace Microsoft.Xna.Framework // to not try and set the window position because it will be wrong. if ((Sdl.Patch > 4 || !AllowUserResizing) && !_wasMoved) Sdl.Window.SetPosition(Handle, centerX, centerY); - + Sdl.Window.Show(Handle); Sdl.Window.Raise(Handle); @@ -333,6 +333,11 @@ namespace Microsoft.Xna.Framework OnTextInput(this, new TextInputEventArgs(c, key)); } + public void DropFile(string filePath) + { + OnFileDropped(new FileDropEventArgs(filePath)); + } + protected internal override void SetSupportedOrientations(DisplayOrientation orientations) { // Nothing to do here diff --git a/README.md b/README.md index 22318bba2..28678353b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Barotrauma -Copyright © Undertow Games 2017-2019 +Copyright © FakeFish Ltd 2017-2021 Before downloading the source code, please read the [EULA](EULA.txt). @@ -12,7 +12,9 @@ If you're interested in working on the code, either to develop mods or to contri **Official Website:** www.barotraumagame.com -**Forums:** http://undertowgames.com/forum/ +**Steam Forums:** https://steamcommunity.com/app/602960/discussions/ + +**Discord:** https://discordapp.com/invite/undertow **Wiki:** https://barotraumagame.com/wiki/Main_Page