using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; using static Barotrauma.CharacterParams; namespace Barotrauma { class PetBehavior { public enum StatusIndicatorType { None, Happy, Sad, Hungry } private float hunger = 50.0f; public float Hunger { get { return hunger; } set { hunger = MathHelper.Clamp(value, 0.0f, MaxHunger); } } private float happiness = 50.0f; public float Happiness { get { return happiness; } set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); } } /// /// At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon) /// public float UnhappyThreshold { get; set; } /// /// At which point is the pet considered "happy" (playing happy sounds and showing the icon) /// public float HappyThreshold { get; set; } public float MaxHappiness { get; set; } /// /// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon) /// public float HungryThreshold { get; set; } public float MaxHunger { get; set; } public float HappinessDecreaseRate { get; set; } public float HungerIncreaseRate { get; set; } public float PlayForce { get; set; } public float PlayTimer { get; set; } private float? UnstunY { get; set; } public EnemyAIController AIController { get; private set; } = null; public Character Owner { get; set; } private class ItemProduction { public struct Item { public ItemPrefab Prefab; public float Commonness; } public List Items; public Vector2 HungerRange; public Vector2 HappinessRange; public float Rate; public float HungerRate; public float InvHungerRate; public float HappinessRate; public float InvHappinessRate; private readonly float totalCommonness; private float timer; public ItemProduction(XElement element) { Items = new List(); HungerRate = element.GetAttributeFloat("hungerrate", 0.0f); InvHungerRate = element.GetAttributeFloat("invhungerrate", 0.0f); HappinessRate = element.GetAttributeFloat("happinessrate", 0.0f); InvHappinessRate = element.GetAttributeFloat("invhappinessrate", 0.0f); string[] requiredHappinessStr = element.GetAttributeString("requiredhappiness", "0-100").Split('-'); string[] requiredHungerStr = element.GetAttributeString("requiredhunger", "0-100").Split('-'); HappinessRange = new Vector2(0, 100); HungerRange = new Vector2(0, 100); float tempF; if (requiredHappinessStr.Length >= 2) { if (float.TryParse(requiredHappinessStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.X = tempF; } if (float.TryParse(requiredHappinessStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.Y = tempF; } } if (requiredHungerStr.Length >= 2) { if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.X = tempF; } if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.Y = tempF; } } Rate = element.GetAttributeFloat("rate", 0.016f); totalCommonness = 0.0f; foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "item": Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); Item newItemToProduce = new Item { Prefab = identifier.IsEmpty ? null : ItemPrefab.Find("", subElement.GetAttributeIdentifier("identifier", Identifier.Empty)), Commonness = subElement.GetAttributeFloat("commonness", 0.0f) }; totalCommonness += newItemToProduce.Commonness; Items.Add(newItemToProduce); break; } } timer = 1.0f; } public void Update(PetBehavior pet, float deltaTime) { if (pet.Happiness < HappinessRange.X || pet.Happiness > HappinessRange.Y) { return; } if (pet.Hunger < HungerRange.X || pet.Hunger > HungerRange.Y) { return; } float currentRate = Rate; currentRate += HappinessRate * (pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X); currentRate += InvHappinessRate * (1.0f - ((pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X))); currentRate += HungerRate * (pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X); currentRate += InvHungerRate * (1.0f - ((pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X))); timer -= currentRate * deltaTime; if (timer <= 0.0f) { timer = 1.0f; float r = Rand.Range(0.0f, totalCommonness); float aggregate = 0.0f; for (int i = 0; i < Items.Count; i++) { aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition); break; } } } } } private class Food { public string Tag; public Vector2 HungerRange; public float Hunger; public float Happiness; public float Priority; public bool IgnoreContained; public CharacterParams.TargetParams TargetParams = null; } private readonly List itemsToProduce = new List(); private readonly List foods = new List(); public PetBehavior(XElement element, EnemyAIController aiController) { AIController = aiController; MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f); UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f); HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f); MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f); HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f); Happiness = MaxHappiness * 0.5f; Hunger = MaxHunger * 0.5f; HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f); HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f); PlayForce = element.GetAttributeFloat("playforce", 15.0f); foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "itemproduction": itemsToProduce.Add(new ItemProduction(subElement)); break; case "eat": Food food = new Food { Tag = subElement.GetAttributeString("tag", ""), Hunger = subElement.GetAttributeFloat("hunger", -1), Happiness = subElement.GetAttributeFloat("happiness", 1), Priority = subElement.GetAttributeFloat("priority", 100), IgnoreContained = subElement.GetAttributeBool("ignorecontained", true) }; string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-'); food.HungerRange = new Vector2(0, 100); if (requiredHungerStr.Length >= 2) { if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; } if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; } } foods.Add(food); break; } } GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); } public StatusIndicatorType GetCurrentStatusIndicatorType() { if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; } if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; } if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; } return StatusIndicatorType.None; } public bool OnEat(Item item) { bool success = OnEat(item.GetTags()); if (success) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier); } return success; } public bool OnEat(Character character) { if (character == null || !character.IsDead) { return false; } bool success = OnEat("dead".ToIdentifier()); if (success) { GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName); } return success; } private bool OnEat(IEnumerable tags) { foreach (Identifier tag in tags) { if (OnEat(tag)) { return true; } } return false; } public bool OnEat(Identifier tag) { for (int i = 0; i < foods.Count; i++) { if (tag == foods[i].Tag) { Hunger += foods[i].Hunger; Happiness += foods[i].Happiness; #if CLIENT AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); #endif return true; } } return false; } public void Play(Character player) { if (PlayTimer > 0.0f) { return; } Owner ??= player; PlayTimer = 5.0f; AIController.Character.IsRagdolled = true; Happiness += 10.0f; AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); UnstunY = AIController.Character.SimPosition.Y; #if CLIENT AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); #endif } public string GetTagName() { if (AIController.Character.Inventory != null) { foreach (Item item in AIController.Character.Inventory.AllItems) { var tag = item.GetComponent(); if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) { return tag.WrittenName; } } } return string.Empty; } public void Update(float deltaTime) { var character = AIController.Character; if (character?.Removed ?? true || character.IsDead) { return; } if (UnstunY.HasValue) { if (PlayTimer > 4.0f) { float extent = character.AnimController.MainLimb.body.GetMaxExtent(); if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) && character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f) { character.IsRagdolled = false; UnstunY = null; } else { character.IsRagdolled = true; } } else { character.IsRagdolled = false; UnstunY = null; } } PlayTimer -= deltaTime; if (GameMain.NetworkMember?.IsClient ?? false) { return; } if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; } Hunger += HungerIncreaseRate * deltaTime; Happiness -= HappinessDecreaseRate * deltaTime; for (int i = 0; i < foods.Count; i++) { Food food = foods[i]; if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y) { if (food.TargetParams == null) { if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target)) { food.TargetParams = target; } else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams)) { food.TargetParams = targetParams; } if (food.TargetParams != null) { food.TargetParams.State = AIState.Eat; food.TargetParams.Priority = food.Priority; food.TargetParams.IgnoreContained = food.IgnoreContained; } } } else if (food.TargetParams != null) { AIController.AIParams.RemoveTarget(food.TargetParams); food.TargetParams = null; } } if (Hunger >= MaxHunger * 0.99f) { character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime)); } if (character.SelectedBy != null) { character.IsRagdolled = true; UnstunY = character.SimPosition.Y; } for (int i = 0; i < itemsToProduce.Count; i++) { itemsToProduce[i].Update(this, deltaTime); } } public static void SavePets(XElement petsElement) { foreach (Character c in Character.CharacterList) { if (!c.IsPet || c.IsDead) { continue; } if (c.Submarine == null) { continue; } var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior; if (petBehavior == null) { continue; } XElement petElement = new XElement("pet", new XAttribute("speciesname", c.SpeciesName), new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0), new XAttribute("seed", c.Seed)); var petBehaviorElement = new XElement("petbehavior", new XAttribute("hunger", petBehavior.Hunger.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("happiness", petBehavior.Happiness.ToString("G", CultureInfo.InvariantCulture))); petElement.Add(petBehaviorElement); var healthElement = new XElement("health"); c.CharacterHealth.Save(healthElement); petElement.Add(healthElement); if (c.Inventory != null) { var inventoryElement = new XElement("inventory"); Character.SaveInventory(c.Inventory, inventoryElement); petElement.Add(inventoryElement); } petsElement.Add(petElement); } } public static void LoadPets(XElement petsElement) { foreach (var subElement in petsElement.Elements()) { string speciesName = subElement.GetAttributeString("speciesname", ""); string seed = subElement.GetAttributeString("seed", "123"); int ownerHash = subElement.GetAttributeInt("ownerhash", 0); Vector2 spawnPos = Vector2.Zero; Character owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == ownerHash); if (owner != null && owner.Submarine?.Info.Type == SubmarineType.Player) { spawnPos = owner.WorldPosition; } else { //try to find a spawnpoint in the main sub var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced(); //if not found, try any player sub (shuttle/drone etc) spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier()); if (characterPrefab == null) { DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found."); continue; } var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false); if (pet != null) { var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior; if (petBehavior != null) { petBehavior.Owner = owner; var petBehaviorElement = subElement.Element("petbehavior"); if (petBehaviorElement != null) { petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); } } } var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); } } } } }