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/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 20feee440..09a5ea3b3 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(); @@ -859,7 +861,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..51f691267 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); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 3f1b518ac..70a61e811 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 @@ -354,56 +353,88 @@ 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(); - //255 = entity already removed, no need to do anything if (attackLimbIndex == 255 || Removed) { break; } - + Vector2 targetSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); 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(); (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); 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(0, orderPrefab.Options.Length); + option = orderPrefab.Options[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); + foreach (Item item in Inventory.AllItems) + { + item.AllowStealing = true; } - GameMain.GameSession.CrewManager.SetHighlightedOrderIcon(this, orderPrefab.Identifier, option); break; } msg.ReadPadBits(); @@ -471,7 +502,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 +516,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..f8bfcc7f0 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(); + 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 != null ? emitter.Prefab.ScaleMin : 0.5f; float particleMaxScale = emitter != null ? emitter.Prefab.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); @@ -742,6 +746,7 @@ namespace Barotrauma float radialDistortStrength = 0.0f; float chromaticAberrationStrength = 0.0f; float grainStrength = 0.0f; + Color grainColor = Color.White; if (Character.IsUnconscious) { @@ -750,10 +755,15 @@ namespace Barotrauma } else if (OxygenAmount < 100.0f) { + // TODO disable some of these? blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality); distortStrength = blurStrength; distortSpeed = (blurStrength + 1.0f); distortSpeed *= distortSpeed * distortSpeed * distortSpeed; + + + grainStrength = MathHelper.Lerp(0.5f, 10.0f, 1.0f - (OxygenAmount - LowOxygenThreshold) / LowOxygenThreshold); + grainColor = oxygenLowGrainColor; } foreach (Affliction affliction in afflictions) @@ -778,6 +788,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; @@ -2004,7 +2015,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 +2082,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..47a0ddbad 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 { @@ -1367,6 +1368,237 @@ 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 (Tuple itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + { + NewMessage(" If bought at " + itemLocationPrice.Item1 + " it costs " + itemLocationPrice.Item2.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 (Tuple ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + { + if (basePrice > ingredientItemLocationPrice.Item2.Price) + { + NewMessage(" Location " + ingredientItemLocationPrice.Item1 + " sells ingredient " + ingredientItemPrefab.Name + " for cheaper, " + ingredientItemLocationPrice.Item2.Price, Color.Yellow); + } + else + { + NewMessage(" Location " + ingredientItemLocationPrice.Item1 + " sells ingredient " + ingredientItemPrefab.Name + " for more, " + ingredientItemLocationPrice.Item2.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.Item2.Price - totalBestPrice; + NewMessage(" Constructing the item from store-bought items provides " + bestDifference + " profit with best-case scenario values."); + } + } + } + }, 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 +3167,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/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/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..4134996da --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/EscortMission.cs @@ -0,0 +1,32 @@ +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++) + { + 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 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/PirateMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..4391a2d05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs @@ -0,0 +1,33 @@ +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..e7d1b227d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1331,7 +1331,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 +1339,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) @@ -1395,6 +1395,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 +1501,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 +1993,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 @@ -2221,6 +2267,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 +2300,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 +2337,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/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/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/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 3b0679933..fac0477eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -947,7 +947,7 @@ namespace Barotrauma { 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 3b3d5ceff..73ce812e5 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 CampaignMode? Campaign => campaignUI.Campaign; + private int AvailableMoney => Campaign?.Money ?? 0; private UpgradeTab selectedUpgradTab = 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 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; @@ -103,29 +112,41 @@ namespace Barotrauma { switch (selectedUpgradTab) { - case UpgradeTab.Repairs: - { + case UpgradeTab.Repairs: SelectTab(UpgradeTab.Repairs); - break; - } - case UpgradeTab.Upgrade: - { + break; + case UpgradeTab.Upgrade: RefreshUpgradeList(); - break; - } + foreach (var itemPreview in itemPreviews) + { + if (itemPreview.Key?.PendingItemSwap?.UpgradePreviewSprite == null) { continue; } + if (!(itemPreview.Value is GUIImage image)) { continue; } + 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,16 +156,18 @@ 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); } } @@ -252,7 +275,7 @@ 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) => @@ -304,12 +327,12 @@ namespace Barotrauma { if (currentStoreLayout != null) { - storeLayout.RemoveChild(currentStoreLayout); + storeLayout?.RemoveChild(currentStoreLayout); } if (selectedUpgradeCategoryLayout != null) { - mainStoreLayout.RemoveChild(selectedUpgradeCategoryLayout); + mainStoreLayout?.RemoveChild(selectedUpgradeCategoryLayout); } switch (tab) @@ -329,8 +352,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 +380,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 +414,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 +459,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 +485,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 +502,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 +523,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) @@ -594,6 +620,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,16 +632,16 @@ namespace Barotrauma if (!component.Enabled) { selectedUpgradeCategoryLayout?.ClearChildren(); - foreach (GUIFrame itemFrame in itemPreviews.Values) + foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = previewWhite; } 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); } return true; @@ -624,35 +651,296 @@ 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 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; } 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 = Submarine.MainSub.GetItems(true).Any(i => + i.Prefab.SwappableItem != null && (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))); + + 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 upgradeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.upgrades"), style: "GUITabButton") + { + Selected = true + }; + + GUIButton customizeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.customize"), style: "GUITabButton"); + + 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; + } + 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; + IEnumerable availableReplacements = MapEntityPrefab.List.Where(p => + p is ItemPrefab itemPrefab && + category.ItemTags.Any(t => itemPrefab.Tags.Contains(t)) && + (itemPrefab.SwappableItem?.CanBeBought ?? false)).Cast(); + var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))).ToList(); + + int slotIndex = 0; + foreach (Item item in entitiesOnSub) + { + slotIndex++; + CreateSwappableItemSlideDown(parent, slotIndex, item, availableReplacements); + } + } + + private void CreateSwappableItemSlideDown(GUIListBox parent, int slotIndex, Item item, IEnumerable availableReplacements) + { + if (Campaign == null) { return; } + + 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.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: "StoreRemoveFromCrateButton")); + + 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, null, + addBuyButton: true, + addProgressBar: false, + buttonStyle: isPurchased ? "UpgradeBuyButton" : "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 +950,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 +996,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 +1030,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 +1062,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 +1080,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 +1091,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 +1176,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) { @@ -875,7 +1193,20 @@ namespace Barotrauma HoveredItem = item; if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == 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 && !itemElement.Selected) + { + itemElement.OnClicked(itemElement, itemElement.UserData); + } + } + } + else + { + ScrollToCategory(data => data.Category.CanBeApplied(item, null)); + } } found = true; break; @@ -888,11 +1219,11 @@ 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 (HoveredItem != firstStructure) { CreateItemTooltip(firstStructure); } + if (HoveredItem != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } HoveredItem = firstStructure; isMouseOnStructure = true; GUI.MouseCursor = CursorState.Hand; @@ -917,6 +1248,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 +1294,32 @@ 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 + }; + } + 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); @@ -1093,7 +1443,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 +1498,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 +1515,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 +1537,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..25ba13501 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() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index c52aa864f..5fef339b4 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; } @@ -1272,7 +1307,7 @@ namespace Barotrauma 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 +1329,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 +1556,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 +1619,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 @@ -2126,6 +2282,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 +2296,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 +2309,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 +2319,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 +2328,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)); @@ -2645,16 +2821,10 @@ 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); optionNodes.Add(new Tuple(optionElement, Keys.None)); } optionElements.Add(optionElement); @@ -3182,16 +3352,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 +3364,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 +3467,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..84952177c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -326,7 +326,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 +483,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)); } } @@ -569,6 +570,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 @@ -657,6 +665,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) @@ -703,6 +726,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..469790d11 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(); } @@ -480,6 +482,8 @@ namespace Barotrauma EnableRoundSummaryGameOverState(); } + CrewManager?.ClearCurrentOrders(); + //-------------------------------------- SelectSummaryScreen(roundSummary, newLevel, mirror, () => @@ -735,9 +739,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 +753,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/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/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..97b6dc4e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -311,10 +311,11 @@ namespace Barotrauma } 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()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 8120ab775..b1beafe96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -910,7 +910,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; } 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/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/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..c5a72e0f7 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; @@ -459,8 +444,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--) { @@ -1101,19 +1091,14 @@ 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 (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 +1614,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 +1636,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..0cf62bd4b 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.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/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/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 67772a0a2..c5806925f 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"), @@ -147,9 +138,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..b7e45630f 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.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(1f, 2f, 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 (Tuple chargeSprite in chargeSprites) + { + chargeSprite.Item1?.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(chargeSprite.Item2.X * chargeRatio, chargeSprite.Item2.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + item.SpriteColor, + rotation + MathHelper.PiOver2, item.Scale, + SpriteEffects.None, item.SpriteDepth + (chargeSprite.Item1.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 = spinningBarrel.Depth + (barrelCirclePosition > HalfCircle ? -0.001f : 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/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index a328939a1..99960ef81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -320,6 +320,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) { @@ -1575,7 +1580,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..751dbda41 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; @@ -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; } }; @@ -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())) + { + continue; + } } else { @@ -1093,8 +1130,9 @@ namespace Barotrauma nameText += $" ({idName})"; } } - texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + bool noComponentText = true; foreach (ItemComponent ic in components) { if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } @@ -1114,8 +1152,13 @@ namespace Barotrauma } } texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); + noComponentText = false; } - if ((PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) && CrewManager.DoesItemHaveContextualOrders(this)) + if (noComponentText && CampaignInteractionType != CampaignMode.InteractionType.None) + { + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameMain.Config.KeyBindText(InputType.Use)), Color.Cyan, false, false)); + } + if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) { texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false, false)); } @@ -1213,6 +1256,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 +1373,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; 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/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 03649abd1..a083a5e89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -97,6 +97,8 @@ namespace Barotrauma { get { return selectedList.Contains(this); } } + + public bool IsIncludedInSelection { get; set; } public virtual bool IsVisible(Rectangle worldView) { @@ -360,6 +362,11 @@ namespace Barotrauma selectionSize.X = position.X - selectionPos.X; selectionSize.Y = selectionPos.Y - position.Y; + foreach (MapEntity entity in mapEntityList) + { + entity.IsIncludedInSelection = false; + } + List newSelection = new List();// FindSelectedEntities(selectionPos, selectionSize); if (Math.Abs(selectionSize.X) > Submarine.GridSize.X || Math.Abs(selectionSize.Y) > Submarine.GridSize.Y) { @@ -372,10 +379,15 @@ namespace Barotrauma if (SelectionGroups.TryGetValue(highLightedEntity, out List group)) { newSelection.AddRange(group); + foreach (MapEntity entity in group) + { + entity.IsIncludedInSelection = true; + } } else { newSelection.Add(highLightedEntity); + highLightedEntity.IsIncludedInSelection = true; } } } @@ -443,6 +455,10 @@ namespace Barotrauma selectionPos = Vector2.Zero; selectionSize = Vector2.Zero; + foreach (MapEntity entity in mapEntityList) + { + entity.IsIncludedInSelection = false; + } } } //default, not doing anything specific yet @@ -800,7 +816,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); } } @@ -1141,7 +1182,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..8dfdd6452 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; } }; @@ -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..990db04c4 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 if (orderMessageInfo.TargetCharacter != null) { - 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..bd4317a2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1046,6 +1046,11 @@ namespace Barotrauma.Networking mission.ClientReadInitial(inc); } + if (inc.ReadBoolean()) + { + CrewManager.ClientReadActiveOrders(inc); + } + roundInitStatus = RoundInitStatus.Started; } @@ -1609,6 +1614,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 +1626,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 +1656,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) { @@ -3289,16 +3296,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..2753462b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -745,6 +745,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..f3235e76d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -66,7 +66,7 @@ namespace Barotrauma.Particles public Vector4 ColorMultiplier; public bool DrawOnTop { get; private set; } - + public ParticlePrefab.DrawTargetType DrawTarget { get { return prefab.DrawTarget; } @@ -103,8 +103,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})"; @@ -118,6 +117,16 @@ namespace Barotrauma.Particles 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; @@ -138,10 +147,6 @@ namespace Barotrauma.Particles totalLifeTime = prefab.LifeTime; lifeTime = prefab.LifeTime; 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); color = prefab.StartColor; changeColor = prefab.StartColor != prefab.EndColor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 0335ba0c7..db7168738 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -1,10 +1,21 @@ using Microsoft.Xna.Framework; using System; -using System.Linq; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Particles { + class ParticleEmitterProperties : ISerializableEntity + { + public string Name => nameof(ParticleEmitterProperties); + public Dictionary SerializableProperties { get; } + + public ParticleEmitterProperties(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + class ParticleEmitter { private float emitTimer; @@ -23,7 +34,7 @@ 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) { emitTimer += deltaTime * amountMultiplier; burstEmitTimer -= deltaTime; @@ -33,7 +44,7 @@ namespace Barotrauma.Particles float emitInterval = 1.0f / Prefab.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; } } @@ -43,11 +54,11 @@ namespace Barotrauma.Particles burstEmitTimer = Prefab.EmitInterval; for (int i = 0; i < Prefab.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); @@ -55,11 +66,12 @@ namespace Barotrauma.Particles Vector2 velocity = dir * Rand.Range(Prefab.VelocityMin, Prefab.VelocityMax) * velocityMultiplier; position += dir * Rand.Range(Prefab.DistanceMin, Prefab.DistanceMax); - var particle = GameMain.ParticleManager.CreateParticle(overrideParticle ?? Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop); + var particle = GameMain.ParticleManager.CreateParticle(overrideParticle ?? Prefab.ParticlePrefab, position, velocity, particleRotation, hullGuess, Prefab.DrawOnTop, tracerPoints: tracerPoints); if (particle != null) { particle.Size *= Rand.Range(Prefab.ScaleMin, Prefab.ScaleMax) * sizeMultiplier; + particle.Size *= Prefab.ScaleMultiplier; particle.HighQualityCollisionDetection = Prefab.HighQualityCollisionDetection; if (colorMultiplier.HasValue) { @@ -135,6 +147,7 @@ namespace Barotrauma.Particles public readonly float VelocityMin, VelocityMax; public readonly float ScaleMin, ScaleMax; + public readonly Vector2 ScaleMultiplier; public readonly float EmitInterval; public readonly int ParticleAmount; @@ -179,6 +192,7 @@ namespace Barotrauma.Particles ScaleMin = element.GetAttributeFloat("scalemin", 1.0f); ScaleMax = Math.Max(ScaleMin, element.GetAttributeFloat("scalemax", 1.0f)); } + ScaleMultiplier = element.GetAttributeVector2("scalemultiplier", Vector2.One); if (element.Attribute("distance") == null) { 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/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/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 92698cec8..234b8fce8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -505,7 +505,7 @@ namespace Barotrauma missionName.CalculateHeightFromText(); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(), wrap: true, parseRichText: true); + 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); @@ -517,8 +517,9 @@ namespace Barotrauma { var textBlock = child as GUITextBlock; textBlock.Color = textBlock.SelectedColor = textBlock.HoverColor = Color.Transparent; - textBlock.HoverTextColor = textBlock.TextColor; + textBlock.SelectedTextColor = textBlock.TextColor; textBlock.TextColor *= 0.5f; + textBlock.HoverTextColor = textBlock.TextColor; } missionPanel.OnAddedToGUIUpdateList = (c) => { 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/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 151e4e858..9bb7804e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -929,7 +929,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 = (_, __) => { @@ -1322,7 +1322,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(); } @@ -3269,7 +3269,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..ef47d565e 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,7 +16,7 @@ using Barotrauma.IO; namespace Barotrauma { - class ParticleEditorScreen : Screen + class ParticleEditorScreen : EditorScreen { class Emitter : ISerializableEntity { @@ -23,22 +24,22 @@ namespace Barotrauma public float BurstTimer; - [Editable(), Serialize("0.0,360.0", false)] + [Editable, Serialize("0.0,360.0", false)] public Vector2 AngleRange { get; private set; } - [Editable(), Serialize("0.0,0.0", false)] + [Editable, Serialize("0.0,0.0", false)] public Vector2 VelocityRange { get; private set; } - [Editable(), Serialize("1.0,1.0", false)] + [Editable, Serialize("1.0,1.0", false)] public Vector2 ScaleRange { get; private set; } - [Editable(), Serialize(0, false)] + [Editable, Serialize(0, false)] public int ParticleBurstAmount { get; private set; } - [Editable(), Serialize(1.0f, false)] + [Editable, Serialize(1.0f, false)] public float ParticleBurstInterval { get; private set; } - [Editable(), Serialize(1.0f, false)] + [Editable, Serialize(1.0f, false)] public float ParticlesPerSecond { get; private set; } public string Name @@ -77,19 +78,24 @@ namespace Barotrauma 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() @@ -314,7 +320,17 @@ namespace Barotrauma public override void Update(double deltaTime) { cam.MoveCamera((float)deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); - + + if (GUI.MouseOn is null && PlayerInput.PrimaryMouseButtonHeld()) + { + sizeRefPosition = cam.ScreenToWorld(PlayerInput.MousePosition); + } + + if (PlayerInput.SecondaryMouseButtonClicked()) + { + CreateContextMenu(); + } + if (selectedPrefab != null) { emitter.EmitTimer += (float)deltaTime; @@ -345,6 +361,15 @@ namespace Barotrauma 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 +382,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 +399,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..4f77b1dff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -363,7 +363,9 @@ namespace Barotrauma "VineSprite", "LeafSprite", "FlowerSprite", - "DecorativeSprite" + "DecorativeSprite", + "BarrelSprite", + "RailSprite" }; 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..7a9da1ca0 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 /// @@ -2020,30 +2068,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 +2261,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 +2362,6 @@ namespace Barotrauma if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } - private void CreateSaveAssemblyScreen() { SetMode(Mode.Default); @@ -2712,8 +2768,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 +2807,6 @@ namespace Barotrauma }; adjustLightsPrompt.Buttons[1].OnClicked += adjustLightsPrompt.Close; } - - return true; } private void TryDeleteSub(SubmarineInfo sub) @@ -2822,7 +2883,7 @@ namespace Barotrauma } } - if (!string.IsNullOrEmpty(entityFilterBox.Text) || dummyCharacter?.SelectedConstruction?.OwnInventory != null) + if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } @@ -2835,7 +2896,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 +2923,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,7 +2999,7 @@ 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)); @@ -2973,10 +3030,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 +3044,7 @@ namespace Barotrauma try { - element = XDocument.Parse(clipboard).Root; + element = XDocument.Parse(text).Root; } catch (Exception) { /* ignored */ } @@ -2996,12 +3054,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) { @@ -3274,57 +3331,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 +3501,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(); @@ -4753,7 +4739,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..ebc8e2705 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() ?? false)) + : SerializableProperty.GetProperties(entity), 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..57d79287a 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 @@ -829,9 +831,23 @@ namespace Barotrauma targetMusic[0] = 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[0] == null || currentMusic[0] == null || !currentMusic[0].IsPlaying() || !suitableMusic.Any(m => m.File == currentMusic[0].Filename)) { - targetMusic[0] = suitableMusic.GetRandom(); + if (currentMusicType == "default") + { + if (previousDefaultMusic == null) + { + targetMusic[0] = previousDefaultMusic = suitableMusic.GetRandom(); + } + else + { + targetMusic[0] = previousDefaultMusic; + } + } + else + { + targetMusic[0] = suitableMusic.GetRandom(); + } } //get the appropriate intensity layers for current situation @@ -851,7 +867,7 @@ 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++) { @@ -973,7 +989,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/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..70605fd5b 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.1400.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 9dcf56a03..a97958ec4 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.1400.0.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..a3d90f8c0 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.1400.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 1f5d68a8f..257e9dd48 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.1400.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 556643a5a..0817a9bb7 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.1400.0.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..290eafdbd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -283,24 +283,25 @@ namespace Barotrauma if (extraData != null) { + 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,71 @@ 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); 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.Options.IndexOf(currentOrderInfo.Value.OrderOption); + msg.WriteRangedInteger(optionIndex, 0, orderPrefab.Options.Length); } - 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); + 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); 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/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index 89446aa82..73620bce3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -22,7 +22,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/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs new file mode 100644 index 000000000..aaeff11f4 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -0,0 +1,31 @@ +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); + List characterItems = characterDictionary[character]; + // items must be written in a specific sequence so that child items aren't written before their parents + msg.Write((ushort)characterItems.Count()); + foreach (Item item in characterItems) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..543caf655 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -0,0 +1,32 @@ +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); + List characterItems = characterDictionary[character]; + // items must be written in a specific sequence so that child items aren't written before their parents + msg.Write((ushort)characterItems.Count()); + foreach (Item item in characterItems) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} 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..838bf1fbf 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,63 @@ 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.Inventory.DeleteAllItems(); + } + } + protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror, List traitorResults) { lastUpdateID++; @@ -186,59 +251,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; @@ -284,6 +297,8 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + CrewManager?.ClearCurrentOrders(); + //-------------------------------------- GameMain.Server.EndGame(transitionType); @@ -497,6 +512,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) { @@ -563,6 +585,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"); @@ -649,6 +686,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 +920,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/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..8ed61ee28 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -537,7 +537,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 +1208,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 +1317,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 +1500,6 @@ namespace Barotrauma.Networking inc.ReadPadBits(); } - private void ClientWrite(Client c) { if (gameStarted && c.InGame) @@ -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) @@ -3314,7 +3321,6 @@ namespace Barotrauma.Networking serverSettings.SaveClientPermissions(); } - private IEnumerable SendClientPermissionsAfterClientListSynced(Client recipient, Client client) { DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); @@ -3331,7 +3337,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/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..cd505875a 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.1400.0.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..1d8424be9 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 @@ + @@ -144,6 +148,7 @@ + @@ -252,4 +257,5 @@ + \ 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/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..a449ae415 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -381,6 +381,7 @@ namespace Barotrauma SelectedAiTarget = target; selectedTargetMemory = GetTargetMemory(target, true); selectedTargetMemory.Priority = priority; + ignoredTargets.Remove(target); } private float movementMargin; @@ -496,7 +497,7 @@ namespace Barotrauma } } - 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 +788,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 { @@ -1203,7 +1204,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)) { @@ -1777,6 +1778,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) { @@ -1893,16 +1895,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; @@ -2184,7 +2198,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; @@ -2610,7 +2624,7 @@ namespace Barotrauma { wallTarget = null; if (State == AIState.Flee || State == AIState.Escape) { return; } - if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; } + if (AIParams.CanOpenDoors && HasValidPath(requireNonDirty: true)) { return; } if (SelectedAiTarget == null) { return; } if (SelectedAiTarget.Entity == null) { return; } Vector2 rayStart = SimPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 4a2e438a9..8b679c678 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,32 @@ 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?.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 +233,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 +318,8 @@ namespace Barotrauma } else { + Character.UpdateTeam(); + if (Character.CurrentHull != null) { if (Character.IsOnPlayerTeam) @@ -301,7 +334,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 +347,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 +428,9 @@ namespace Barotrauma flipTimer = FlipInterval; } } + + MentalStateManager?.Update(deltaTime); + ShipCommandManager?.Update(deltaTime); } private void UnequipUnnecessaryItems() @@ -442,9 +478,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 +489,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 +540,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 +585,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 +638,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)) @@ -743,6 +778,7 @@ namespace Barotrauma { Order newOrder = null; Hull targetHull = null; + bool speak = true; if (Character.CurrentHull != null) { bool isFighting = ObjectiveManager.HasActiveObjective(); @@ -759,6 +795,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 +822,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 +875,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 +1032,11 @@ namespace Barotrauma return; } float cumulativeDamage = GetDamageDoneByAttacker(attacker); - if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null) + if (!Character.IsSecurity && attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null) { if (cumulativeDamage > 1) { - // Don't retaliate on damage done by friendly NPC, because we know it's accidental + // Don't retaliate on damage done by friendly NPC, because we know it's accidental, unless if it's a berserking AI AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } @@ -1039,8 +1094,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); @@ -1070,12 +1128,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 +1163,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 +1205,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 +1241,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 +1263,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 +1346,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. @@ -1464,7 +1538,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()) 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..e2fa4e2b6 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,17 @@ namespace Barotrauma /// public float Priority { get; set; } public float BasePriority { get; set; } - public float PriorityModifier { get; private set; } = 1; + + // For forcing the highest priority temporarily. Will reset after each priority calculation, so it will need to be kept alive by something. + public bool ForceHighestPriority { get; set; } + + // 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 +113,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. /// @@ -217,18 +235,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 +270,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; @@ -393,7 +426,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..cc94b8be1 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 6b99d3899..ec52246bf 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) { @@ -68,7 +68,7 @@ namespace Barotrauma } 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..02c23ffab 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,8 +80,13 @@ 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 && + container.IsInteractable(character) && + container.HasTag("allowcleanup") && + container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && + IsItemInsideValidSubmarine(container, character); public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { @@ -91,7 +96,12 @@ namespace Barotrauma 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..6e372d70e 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,7 +61,7 @@ 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())) @@ -146,7 +146,7 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, - abortCondition = obj => !ItemToContain.IsOwnedBy(character), + AbortCondition = obj => !ItemToContain.IsOwnedBy(character), SpeakIfFails = !objectiveManager.IsCurrentOrder() }, onAbandon: () => Abandon = true, @@ -170,7 +170,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..5039751ff 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,7 +59,7 @@ namespace Barotrauma this.targetContainer = targetContainer; } - protected override bool Check() => IsCompleted; + protected override bool CheckObjectiveSpecific() => IsCompleted; protected override void Act(float deltaTime) { 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..374c22fa2 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,9 +29,9 @@ 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) { @@ -86,21 +86,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 +142,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => { - if (Check()) { IsCompleted = true; } + if (CheckObjectiveSpecific()) { IsCompleted = true; } else { // Failed to operate. Probably too far. @@ -160,7 +161,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 +192,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..49e2978d0 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 45b90df47..dbd640432 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 }; }, @@ -365,19 +367,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; } 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..4df1b7213 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; } @@ -627,7 +635,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 +646,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..677d4666f 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) @@ -100,6 +103,16 @@ 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() || @@ -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..e398263aa 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 6e4046bb5..216df0a91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -8,7 +8,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,7 +31,7 @@ namespace Barotrauma this.isPriority = isPriority; } - public override float GetPriority() + protected override float GetPriority() { if (!IsAllowed || Item.IgnoreByAI) { @@ -71,7 +71,7 @@ namespace Barotrauma return Priority; } - protected override bool Check() + protected override bool CheckObjectiveSpecific() { IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) @@ -122,8 +122,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 +133,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..92d9aa6c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -9,12 +9,7 @@ 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. @@ -28,7 +23,7 @@ namespace Barotrauma 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) @@ -69,16 +64,12 @@ 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)) @@ -88,6 +79,21 @@ namespace Barotrauma return true; } + 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; } } 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..4fcfbfd83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -154,12 +154,15 @@ namespace Barotrauma 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; } @@ -310,8 +313,10 @@ namespace Barotrauma 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 +353,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 +362,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 +380,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 +411,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; 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..e61e4c082 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); } 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..43b76bdc2 --- /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 + DebugConsole.NewMessage($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); +#endif + return false; + } + + if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter)) + { +#if DEBUG + DebugConsole.NewMessage(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..7ece6feb7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs @@ -0,0 +1,32 @@ +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; + } + + if (TargetItem.IgnoreByAI) { 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..07c079bfe --- /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; + } + } + + 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.First(); + + float bestValue = 0f; + Character bestCharacter = null; + + if (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) + { +#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..ed3d7dd97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -55,37 +55,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 +180,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 +330,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 +353,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/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 247b898b4..e5ee2ce1a 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) @@ -478,8 +482,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 +496,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 +516,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 +541,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..4ecae7339 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 @@ -120,6 +126,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 +437,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 +1021,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 +1220,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 +1240,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; } @@ -1595,83 +1696,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 +1853,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; } @@ -1869,24 +1994,39 @@ namespace Barotrauma /// 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 +2035,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 +2169,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) @@ -2360,29 +2507,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) { @@ -2899,10 +3043,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 +3061,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 +3069,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 +3087,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 +3311,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 +3327,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 +3459,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 +3511,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 +3569,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 +3614,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 +3657,7 @@ namespace Barotrauma // OnDamaged is called only for the limb that is hit. AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); } + CharacterHealth.ApplyAfflictionStatusEffects(actionType); } private void Implode(bool isNetworkMessage = false) @@ -3738,8 +3896,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 +3917,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 +4178,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 +4192,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..2c2530c61 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 = "???"; @@ -493,7 +494,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 +502,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 +1020,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..6d99e68df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -69,7 +69,10 @@ namespace Barotrauma if (Strength < DormantThreshold) { DeactivateHusk(); - State = InfectionState.Dormant; + if (Strength > Math.Min(1.0f, DormantThreshold)) + { + State = InfectionState.Dormant; + } } else if (Strength < ActiveThreshold) { 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..300272bb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -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/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index c95a4feb2..5d8d3c27d 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(); } @@ -160,18 +162,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() + { + var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), Rand.RandSync.Unsynced); + 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 +200,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); @@ -237,7 +249,7 @@ namespace Barotrauma } 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..e5591632d 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index ab7c508c4..54235252f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -178,6 +178,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 +203,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 +273,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index c1e9394a4..cb841480c 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)) { @@ -981,13 +986,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 +1151,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..9aa766874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 305baebb7..2fc071d3c 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 @@ -212,7 +215,6 @@ namespace Barotrauma private readonly List filesToAdd; private readonly List filesToRemove; - public IReadOnlyList Files { get { return files; } @@ -609,7 +611,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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 36daaf126..c97e4b208 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -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)); @@ -1885,6 +1904,8 @@ namespace Barotrauma spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy); } + CharacterTeamType teamType; + teamType = args.Length > 2 ? (CharacterTeamType)int.Parse(args[2]) : Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; if (string.IsNullOrWhiteSpace(args[0])) { return; } if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; } @@ -1896,8 +1917,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/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 3e12045f1..8d00c9b7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -240,7 +240,7 @@ namespace Barotrauma { TryStartConversation(speaker); } - else + else if (speaker.ActiveConversation != this) { speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk; speaker.ActiveConversation = this; @@ -352,6 +352,14 @@ namespace Barotrauma 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/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs new file mode 100644 index 000000000..b5ba1cfb0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -0,0 +1,68 @@ +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; + } +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew }); +#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/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 66b9f360a..14105db67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -102,12 +102,12 @@ 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 => + Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { newCharacter.TeamID = CharacterTeamType.FriendlyNPC; newCharacter.EnableDespawn = false; @@ -255,10 +255,9 @@ namespace Barotrauma 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 +266,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 +294,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/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index ceb0da293..f19069422 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,101 @@ 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; +#if CLIENT + npc.SetCustomInteract( + Trigger, + TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use))); +#else + npc.SetCustomInteract( + Trigger, + TextManager.Get("CampaignInteraction.Talk")); + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); +#endif + npc.RequireConsciousnessForCustomInteract = false; + } + + 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; + } + else if (npcOrItem.TryGet(out Item item)) + { + item.CampaignInteractionType = CampaignMode.InteractionType.None; + } + } + private bool IsCloseEnoughToHull(Entity e, out Hull hull) { hull = null; @@ -157,6 +245,7 @@ namespace Barotrauma private void Trigger(Entity entity1, Entity entity2) { + ResetTargetIcons(); if (!string.IsNullOrEmpty(ApplyToTarget1)) { ParentEvent.AddTarget(ApplyToTarget1, entity1); @@ -174,7 +263,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..5d753e12d 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; @@ -694,47 +694,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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 16170a3b0..080b84dcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -149,7 +149,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()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 0fa581532..86ae02a2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -35,8 +35,8 @@ namespace Barotrauma 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"); @@ -84,14 +84,7 @@ namespace Barotrauma 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) - { - DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found"); - continue; - } + HumanPrefab humanPrefab = CreateHumanPrefabFromElement(element); for (int i = 0; i < count; i++) { LoadHuman(humanPrefab, element, submarine); @@ -128,32 +121,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index b3a5365f1..4a1bb12fa 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index f5215e725..a278cfa16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -17,15 +17,97 @@ namespace Barotrauma private int 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 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); + 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,9 +118,9 @@ 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; } @@ -49,7 +131,7 @@ namespace Barotrauma } } - private void LoadItemAsChild(XElement element, Item parent) + private ItemPrefab FindItemPrefab(XElement element) { ItemPrefab itemPrefab; if (element.Attribute("name") != null) @@ -60,7 +142,6 @@ namespace Barotrauma if (itemPrefab == null) { DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found"); - return; } } else @@ -70,15 +151,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 +169,6 @@ namespace Barotrauma } var cargoRoom = cargoSpawnPos.CurrentHull; - if (cargoRoom == null) { DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); 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..e216bc3cc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -0,0 +1,301 @@ +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> characterDictionary = new Dictionary>(); + + private readonly int baseEscortedCharacters; + private readonly float scalingEscortedCharacters; + private readonly float terroristChance; + + 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) + { + characterConfig = prefab.ConfigElement.Element("Characters"); + // Should reflect different escortables, prisoners, VIPs, passengers (where does this comment refer to?) + + baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1); + scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); + terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); + itemConfig = prefab.ConfigElement.Element("TerroristItems"); + } + + public override int Reward + { + get + { + int multiplier = CalculateScalingEscortedCharacterCount(); + return Prefab.Reward * multiplier; + } + } + + int CalculateScalingEscortedCharacterCount(bool inMission = false) + { + if (Submarine.MainSub == null || Submarine.MainSub.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 * Submarine.MainSub.Info.RecommendedCrewSizeMin); + } + + private void InitEscort() + { + characters.Clear(); + characterDictionary.Clear(); + // VIP transport mission characters stay in the same location; other characters roam at will + // could be replaced with a designated waypoint for VIPs, such as cargo or crew + 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; + } + + foreach (XElement element in characterConfig.Elements()) + { + int count = CalculateScalingEscortedCharacterCount(inMission: true); + for (int i = 0; i < count; i++) + { + Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); + if (spawnedCharacter.AIController is HumanAIController humanAI) + { + humanAI.InitMentalStateManager(); + } + } + } + } + + 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++; + } + + if (!IsClient && 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.Shuffle(); + 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 + } + } + + 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; + } + + 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.Any(c => !terroristCharacters.Contains(c) && IsAlive(c)); + } + + public override void Update(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).Any(c => Survived(c)); + bool vipDied = false; + + 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 characterDictionary) + { + if (Survived(characterItem.Key) || !completed) + { + foreach (Item item in characterItem.Value) + { + if (!item.Removed) + { + item.Remove(); + } + } + } + } + + characters.Clear(); + characterDictionary.Clear(); + failed = !completed; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index ef15b10df..54c807e05 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")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index fa3d252da..65e7b67a8 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 { @@ -61,14 +62,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 +98,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(); } @@ -113,7 +129,7 @@ namespace Barotrauma get { return Prefab.Difficulty; } } - public Mission(MissionPrefab prefab, Location[] locations) + public Mission(MissionPrefab prefab, Location[] locations, Submarine sub) { System.Diagnostics.Debug.Assert(locations.Length == 2); @@ -138,8 +154,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++) @@ -181,7 +201,7 @@ namespace Barotrauma { if (randomNumber <= missionPrefab.Commonness) { - return missionPrefab.Instantiate(locations); + return missionPrefab.Instantiate(locations, Submarine.MainSub); } randomNumber -= missionPrefab.Commonness; } @@ -189,6 +209,11 @@ namespace Barotrauma return null; } + public virtual int GetReward(Submarine sub) + { + return Prefab.Reward; + } + public void Start(Level level) { #if CLIENT @@ -232,7 +257,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 +312,48 @@ 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 CreateHumanPrefabFromElement(XElement element) + { + HumanPrefab humanPrefab = null; + + 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 = 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 = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + characterInfo.TeamID = teamType; + Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); + 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..2d0525467 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -20,8 +20,9 @@ namespace Barotrauma Combat = 0x40, OutpostDestroy = 0x80, OutpostRescue = 0x100, - - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue + Escort = 0x200, + Pirate = 0x400, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue | Escort | Pirate } partial class MissionPrefab @@ -38,6 +39,8 @@ namespace Barotrauma { MissionType.Mineral, typeof(MineralMission) }, { MissionType.OutpostDestroy, typeof(OutpostDestroyMission) }, { MissionType.OutpostRescue, typeof(AbandonedOutpostMission) }, + { MissionType.Escort, typeof(EscortMission) }, + { MissionType.Pirate, typeof(PirateMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { @@ -286,16 +289,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 +340,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..4404deba5 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)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index b97ade2ee..c26836fd2 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"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs index 029db236f..6b9505c21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/OutpostDestroyMission.cs @@ -49,8 +49,8 @@ namespace Barotrauma } } - public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) : - base(prefab, locations) + public OutpostDestroyMission(MissionPrefab prefab, Location[] locations, Submarine sub) : + base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.Element("Items"); itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); @@ -96,10 +96,10 @@ namespace Barotrauma spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); } Vector2 spawnPos = spawnPoint.WorldPosition; - if (spawnPoint is WayPoint wp && wp.CurrentHull != null) + 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, wp.CurrentHull.WorldRect.Right), + 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs new file mode 100644 index 000000000..c1cff2ab7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -0,0 +1,290 @@ +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 characterConfig; + private readonly XElement submarineConfig; + + private Submarine enemySub; + private Item reactorItem; + private readonly List characters = new List(); + private readonly Dictionary> characterDictionary = 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; + } + } + } + + private SubmarineInfo submarineInfo; + + public override SubmarineInfo EnemySubmarineInfo => submarineInfo; + + public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + submarineConfig = prefab.ConfigElement.Element("Submarine"); + characterConfig = prefab.ConfigElement.Element("Characters"); + + string submarineIdentifier = submarineConfig.GetAttributeString("identifier", string.Empty); + + if (submarineIdentifier == string.Empty) + { + DebugConsole.ThrowError("No identifier used for submarine for pirate mission!"); + return; + } + // maybe a little redundant + var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarineIdentifier); + if (contentFile == null) + { + DebugConsole.ThrowError("No submarine file found with the identifier!"); + return; + } + submarineInfo = new SubmarineInfo(contentFile.Path); + } + + 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(); + reactorItem = reactor.Item; + } + enemySub.EnableMaintainPosition(); + enemySub.SetPosition(spawnPos); + enemySub.TeamID = CharacterTeamType.None; + } + + private void InitPirates() + { + characters.Clear(); + characterDictionary.Clear(); + + if (characterConfig == null) + { + DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)"); + return; + } + + bool commanderAssigned = false; + foreach (XElement element in characterConfig.Elements()) + { + Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, enemySub, CharacterTeamType.None, null); + if (!commanderAssigned) + { + bool isCommander = element.GetAttributeBool("iscommander", false); + if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController) + { + humanAIController.InitShipCommandManager(); + foreach (var patrolPos in patrolPositions) + { + humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos); + } + commanderAssigned = true; + } + } + } + } + + 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); + } + + // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections to the submarine + // creating the pirates have 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(); + } + } + + public override void Update(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)) || reactorItem.Condition <= 0f); + + private bool Survived(Character character) + { + return character != null && !character.Removed && !character.IsDead; + } + + public override void End() + { + if (state == 2) + { + GiveReward(); + completed = true; + } + characters.Clear(); + characterDictionary.Clear(); + failed = !completed; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 4738a3a66..727e48c4b 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", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index e7bf21f0e..8eef6f234 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) { @@ -192,8 +192,8 @@ namespace Barotrauma 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 +264,7 @@ namespace Barotrauma } else { - if (!isSubOrWreck) + if (!isRuinOrWreck) { float minDistance = 20000; var refSub = GetReferenceSub(); @@ -375,7 +375,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 +387,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 +415,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 +435,15 @@ 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)) + { + scatterAmount = Math.Min(scatter, Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath).Min(t => t.MinWidth) / 2); + } + else if (!spawnPosType.HasFlag(Level.PositionType.MainPath)) + { + scatterAmount = 100; + } for (int i = 0; i < amount; i++) { string seed = Level.Loaded.Seed + i.ToString(); @@ -443,8 +454,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 > 100) { 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..11ddb0f12 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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 0c9cf2706..b80599722 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) + { + var controllingCharacter = controlledCharacter != null; +#if !DEBUG + if (!controllingCharacter) { return null; } +#endif + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter) && + (!controllingCharacter || operatingCharacter.CanHearCharacter(controlledCharacter))) + { + return operatingCharacter; + } + return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => !controllingCharacter || 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..59f841f39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -51,7 +51,7 @@ namespace Barotrauma //there can be no events before this time has passed during the 1st campaign round const float FirstRoundEventDelay = 30.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; @@ -169,7 +169,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)); } @@ -268,7 +268,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)); } } } @@ -285,7 +285,7 @@ namespace Barotrauma var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)p.Commonness).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)); } } } @@ -595,7 +595,6 @@ namespace Barotrauma { CrewManager.RemoveCharacterInfo(ci); } - ci?.ClearCurrentOrders(); } foreach (DockingPort port in DockingPort.List) @@ -637,6 +636,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); 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..8aa19c7b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -353,9 +353,14 @@ namespace Barotrauma } } } - if (GameMode is PvPMode && Submarine.MainSubs[1] == null) + + if (Submarine.MainSubs[1] == null) { - Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); + 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index fae47582a..5d9f627d5 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,6 +87,8 @@ 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; @@ -90,7 +104,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 +176,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 +213,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money > price) + if (Campaign.Money >= price) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -184,6 +255,151 @@ 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 (!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.PendingItemSwap != null) + { + CancelItemSwap(itemToRemove); + } + else */ + 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; + spentMoney += price; + + itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab); + if (itemToInstall != null) { 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 +417,17 @@ 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?.Type != LevelData.LevelType.Outpost) + 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; } } @@ -592,7 +801,17 @@ namespace Barotrauma OnUpgradesChanged?.Invoke(); } - public void SavePendingUpgrades(XElement? parent, List upgrades) + public void Save(XElement? parent) + { + if (parent == null) { return; } + + var upgradeManagerElement = new XElement("upgrademanager"); + parent.Add(upgradeManagerElement); + + SavePendingUpgrades(upgradeManagerElement, PendingUpgrades); + } + + private void SavePendingUpgrades(XElement? parent, List upgrades) { if (parent == null) { return; } @@ -647,7 +866,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) 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/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 60b08dd4a..205814cb6 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) { 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..5b48a61e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -151,6 +151,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/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index f6e4ce5bd..854adbf28 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 (item.IgnoreByAI) { 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..547ecf00d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -204,6 +204,8 @@ 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 (item.ParentInventory is CharacterInventory) @@ -235,8 +237,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 +405,26 @@ 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); + } + else + { + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false); + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 6e0073d67..3b4717d8e 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) { @@ -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); @@ -564,6 +567,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 +602,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 8287f8a42..1c497b7c2 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) @@ -659,18 +716,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 +727,7 @@ namespace Barotrauma.Items.Components } if (objective.Override) { - if (MaintainPos || LevelEndSelected || !LevelStartSelected) + if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically) { unsentChanges = true; } @@ -696,13 +742,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/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index a23a6ba9e..d01ffa299 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; @@ -81,7 +80,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); } @@ -189,7 +192,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() { @@ -269,6 +274,7 @@ namespace Barotrauma.Items.Components if (Hitscan) { Vector2 prevSimpos = item.SimPosition; + item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle); DoHitscan(launchDir); if (i < HitScanCount - 1) { @@ -277,7 +283,8 @@ namespace Barotrauma.Items.Components } else { - DoLaunch(launchDir * LaunchImpulse * item.body.Mass); + float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); + DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); } } User = character; @@ -331,7 +338,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 +389,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 +398,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(); @@ -605,6 +622,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 +634,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,6 +648,12 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Limb limb) { + // when hitting limbs with piercing ammo, don't lose as much speed + if (MaxTargetsToHit > 1) + { + projectileNewSpeed = 1f; + projectileDeflectedNewSpeed = 0.8f; + } //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; } @@ -690,8 +718,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 +759,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 +789,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; @@ -876,5 +904,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..4834c6d92 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; @@ -224,32 +226,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/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index f6ca3513c..5eee68c50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -19,7 +19,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).")] @@ -153,7 +154,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; @@ -171,6 +172,30 @@ namespace Barotrauma.Items.Components float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); + if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null && + (Target == TargetType.Wall || Target == TargetType.Any) && + (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) + { + 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) + { + var closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, item.WorldPosition); + if (Math.Abs(closestPoint.X - item.WorldPosition.X) < rangeX && Math.Abs(closestPoint.Y - item.WorldPosition.Y) < rangeY) + { + MotionDetected = true; + return; + } + } + } + } + foreach (Character c in Character.CharacterList) { if (IgnoreDead && c.IsDead) { continue; } @@ -187,6 +212,8 @@ namespace Barotrauma.Items.Components case TargetType.Monster: if (c.IsHuman || c.IsPet) { continue; } break; + case TargetType.Wall: + break; } //do a rough check based on the position of the character's collider first @@ -203,7 +230,7 @@ namespace Barotrauma.Items.Components if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { MotionDetected = true; - break; + return; } } } 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/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 11740b981..795d8203f 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 List> chargeSprites = new List>(); + private 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 Tuple(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,7 @@ namespace Barotrauma.Items.Components Projectile projectileComponent = projectile.GetComponent(); if (projectileComponent != null) { - projectileComponent.Use((float)Timing.Step); + projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; @@ -780,7 +915,6 @@ 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 && @@ -790,7 +924,7 @@ namespace Barotrauma.Items.Components character.AIController.SelectTarget(null); } - if (GetAvailableBatteryPower() < powerConsumption) + if (!HasPowerToShoot()) { var batteries = item.GetConnectedComponents(); @@ -868,11 +1002,12 @@ 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"), null, 0.0f, "outofammo", 30.0f); } else if (remainingAmmo < 3) { @@ -891,24 +1026,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 +1097,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 +1150,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 +1194,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); @@ -1088,10 +1257,22 @@ namespace Barotrauma.Items.Components { character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); } + aiTargetingGraceTimer = 5f; character.SetInput(InputType.Shoot, true, true); return false; } + private Vector2 GetRelativeFiringPosition(bool useOffset = true) + { + // i don't feel great about this method, should be evaluated again + 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 +1297,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 +1328,7 @@ namespace Barotrauma.Items.Components if (projectileComponent != null && projectileComponent.Item.body != null) { projectiles.Add(projectileComponent); - if (returnFirst) { return; } + return; } else { @@ -1152,9 +1339,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 +1460,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..a1f872c6f 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 @@ -175,6 +178,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) { @@ -313,6 +317,11 @@ namespace Barotrauma.Items.Components 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++ ) { 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..7881c45b8 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,9 @@ 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())) + ) { hasInGameEditableProperties = true; break; @@ -609,12 +626,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; } } @@ -663,6 +680,10 @@ namespace Barotrauma public BallastFloraBranch Infector { get; set; } + public ItemPrefab PendingItemSwap { get; set; } + + public readonly HashSet AvailableSwaps = new HashSet(); + public override string ToString() { #if CLIENT @@ -765,6 +786,7 @@ namespace Barotrauma case "deconstruct": case "brokensprite": case "decorativesprite": + case "upgradepreviewsprite": case "price": case "levelcommonness": case "suitabletreatment": @@ -779,6 +801,7 @@ namespace Barotrauma case "minimapicon": case "infectedsprite": case "damagedinfectedsprite": + case "swappableitem": break; case "staticbody": StaticBodyConfig = subElement; @@ -805,7 +828,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,14 +1065,20 @@ 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(); } @@ -1382,8 +1419,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) @@ -2064,7 +2104,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 +2114,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 +2385,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 +2464,16 @@ namespace Barotrauma } } - private CoroutineHandle logPropertyChangeCoroutine; + private List> GetInGameEditableProperties() + { + return GetProperties() + .Where(ce => ce.Second.GetAttribute().IsEditable()) + .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 +2624,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 +2657,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 +2668,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 +2681,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,36 +2714,36 @@ 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) @@ -2692,12 +2751,37 @@ namespace Barotrauma //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); + } + } + if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); } if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); } + if (appliedSwap != null && 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(); + } + float condition = element.GetAttributeFloat("condition", item.MaxCondition); item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition); item.lastSentCondition = item.condition; @@ -2726,12 +2810,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..18716d19c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -192,6 +192,45 @@ namespace Barotrauma } } + class SwappableItem + { + public int BasePrice { get; } + + public readonly bool CanBeBought; + + public readonly string ReplacementOnUninstall; + + public readonly Vector2 SwapOrigin; + + public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); + + 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); + CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); + ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); + SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "swapconnecteditem": + ConnectedItemsToSwap.Add( + (subElement.GetAttributeString("tag", ""), + subElement.GetAttributeString("swapto", ""))); + break; + } + } + } + } + partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier { private readonly string name; @@ -461,6 +500,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) @@ -830,6 +875,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 +918,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 +1019,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 +1203,54 @@ namespace Barotrauma } } + public List> GetBuyPricesUnder(int maxCost = 0) + { + List> priceLocations = new List>(); + 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(new Tuple(locationPrice.Key, priceInfo)); + } + } + return priceLocations; + } + + public List> GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + { + List> priceLocations = new List>(); + + if (!CanBeSold && sellingImportant) + { + return priceLocations; + } + + foreach (KeyValuePair locationPrice in locationPrices) + { + PriceInfo priceInfo = locationPrice.Value; + + if (priceInfo == null) + { + continue; + } + if (priceInfo.Price > minCost) + { + priceLocations.Add(new Tuple(locationPrice.Key, priceInfo)); + } + } + return priceLocations; + } + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 2703357f7..eee503110 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 50e8b87c2..e73d6468a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -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 @@ -2710,14 +2712,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 +2764,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 +2840,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); @@ -3094,12 +3108,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; } @@ -3306,18 +3319,19 @@ 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, Rand.RandSync.Server); 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 +3379,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) { 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/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 238743d34..3ab9b89e8 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) { } @@ -133,10 +139,24 @@ namespace Barotrauma suitableSpawnPositions.Add(prefab, availableSpawnPositions.Where(sp => sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) && - sp.Length >= prefab.MinSurfaceWidth && + sp.Length >= prefab.MinSurfaceWidth && + (prefab.AllowAtStart || !closeToStart(sp.GraphEdge.Center)) && + (prefab.AllowAtEnd || !closeToEnd(sp.GraphEdge.Center)) && (sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList()); + spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); + + bool closeToStart(Vector2 position) + { + float minDist = level.Size.X * 0.2f; + return MathUtils.LineSegmentToPointDistanceSquared(level.StartPosition.ToPoint(), level.StartExitPosition.ToPoint(), position.ToPoint()) < minDist * minDist; + } + bool closeToEnd(Vector2 position) + { + float minDist = level.Size.X * 0.2f; + return MathUtils.LineSegmentToPointDistanceSquared(level.EndPosition.ToPoint(), level.EndExitPosition.ToPoint(), position.ToPoint()) < minDist * minDist; + } } SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); @@ -484,6 +504,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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 139837000..d383f421c 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. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 3fbe4be14..432d5b60a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -140,6 +140,11 @@ namespace Barotrauma get; private set; } + public float GlobalForceDecreaseInterval + { + get; + private set; + } private readonly TriggerForceMode forceMode; public TriggerForceMode ForceMode @@ -234,6 +239,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 +440,8 @@ namespace Barotrauma } } + private readonly List targets = new List(); + public void Update(float deltaTime) { if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } @@ -457,7 +465,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()) @@ -533,8 +547,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/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index f5d8712f3..9f76aa1e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -533,8 +533,24 @@ 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; + } + } + return weight; + } return InstantiateMission(prefab, connection); } @@ -542,14 +558,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; } @@ -570,7 +586,7 @@ 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; } } 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..2734b83dd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -280,7 +280,7 @@ namespace Barotrauma public void ResolveLinks(IdRemap childRemap) { if (unresolvedLinkedToID == null) { return; } - for (int i=0;i 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 +1547,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.freeSlots))); element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); @@ -1537,6 +1559,40 @@ 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) + { + 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..796e4f0f5 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)); @@ -513,7 +518,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 97c714f73..33a2d6481 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -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); @@ -574,7 +586,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--) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 0d3bf3e32..c82adf09c 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); 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/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..a09c710fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -191,11 +191,11 @@ namespace Barotrauma.Networking { shuttleSteering.SetDestinationLevelStart(); } - UpdateReturningProjSpecific(); + UpdateReturningProjSpecific(deltaTime); } } - partial void UpdateReturningProjSpecific(); + partial void UpdateReturningProjSpecific(float deltaTime); private IEnumerable ForceShuttleToPos(Vector2 position, float speed) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 528f328bd..f0e7e3349 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 { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 4f87592ca..ab646b389 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,34 @@ namespace Barotrauma { } + [AttributeUsage(AttributeTargets.Property)] + class ConditionallyEditable : Editable + { + public ConditionallyEditable(ConditionType conditionType) + { + this.conditionType = conditionType; + } + + private 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 + } + + public bool IsEditable() + { + switch (conditionType) + { + case ConditionType.AllowLinkingWifiToChat: + return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; + } + return false; + } + } + [AttributeUsage(AttributeTargets.Property)] public class Serialize : Attribute diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 8090206e6..502dbae88 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) { @@ -1235,6 +1237,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/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/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..1762ca22c 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..b1ad35355 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..d4796a526 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/DugongChaingun.sub b/Barotrauma/BarotraumaShared/Submarines/DugongChaingun.sub new file mode 100644 index 000000000..12fb83660 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/DugongChaingun.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/DugongPulseLaser.sub b/Barotrauma/BarotraumaShared/Submarines/DugongPulseLaser.sub new file mode 100644 index 000000000..90d71e0ff Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/DugongPulseLaser.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/HumpbackPirate2.0.sub b/Barotrauma/BarotraumaShared/Submarines/HumpbackPirate2.0.sub new file mode 100644 index 000000000..ed1d89f97 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/HumpbackPirate2.0.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 5e14feb89..695dfc9dd 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index a8cd9ff9f..9b5517749 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..4bfa0e876 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..fd371a320 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index b0fb3144b..5ccacb192 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..bdd414450 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..1e2dab5ae 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,55 @@ +------------------------------------------------------------------------------------------------------ +v0.1400.0.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Changes: +- Submarine weapons can be swapped in the outposts. +- Added Pulse Laser and Chaingun (still WIP). +- Added Canister Shells (a reloadable burst-type munition for the railgun). +- Added escort missions. +- Added pirate missions. +- 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. +- 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...? +- 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. +- Most outpost events no longer trigger automatically, but require interacting with a specific item/character. + +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. + +Modding: +- 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. + ------------------------------------------------------------------------------------------------------ v0.13.3.11 --------------------------------------------------------------------------------------------------------- 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