Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs
2025-09-17 13:44:21 +03:00

534 lines
22 KiB
C#

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.Runtime.InteropServices;
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); }
}
/// <summary>
/// At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon)
/// </summary>
public float UnhappyThreshold { get; set; }
/// <summary>
/// At which point is the pet considered "happy" (playing happy sounds and showing the icon)
/// </summary>
public float HappyThreshold { get; set; }
public float MaxHappiness { get; set; }
public bool HideStatusIndicators { get; set; }
/// <summary>
/// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
/// </summary>
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; }
public float PlayCooldown { get; set; }
/// <summary>
/// Should the pet lose ownership (and stop following) when the same character interacts with it twice? Unlike with other pets, if another character interacts with the pet, they will become the owner.
/// </summary>
public bool ToggleOwner { 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<Item> 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<Item>();
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)
{
//disabled to reduce the amount of data we collect through GA
//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 Identifier Tag;
public Vector2 HungerRange;
public float Hunger;
public float Happiness;
public float Priority;
public bool IgnoreContained;
public CharacterParams.TargetParams TargetParams = null;
}
private readonly List<ItemProduction> itemsToProduce = new List<ItemProduction>();
private readonly List<Food> foods = new List<Food>();
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);
HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false);
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(nameof(PlayForce), 15.0f);
PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f);
ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false);
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.GetAttributeIdentifier("tag", Identifier.Empty),
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 (HideStatusIndicators) { return StatusIndicatorType.None; }
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<Identifier> 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 bool CanPlayWith(Character player)
{
return AIController.Character.IsOnFriendlyTeam(player);
}
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
if (!CanPlayWith(player)) { return; }
if (ToggleOwner)
{
Owner = Owner == player ? null : player;
}
else
{
Owner ??= player;
}
PlayTimer = PlayCooldown;
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(Owner == null ? CharacterSound.SoundType.Unhappy : CharacterSound.SoundType.Happy);
#endif
}
public string GetTagName()
{
if (AIController.Character.Inventory != null)
{
foreach (Item item in AIController.Character.Inventory.AllItems)
{
var tag = item.GetComponent<NameTag>();
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 > PlayCooldown - 1.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.TryGetTargets(food.Tag, out IEnumerable<TargetParams> existingTargetParams))
{
foreach (var targetParams in existingTargetParams)
{
food.TargetParams = targetParams;
}
}
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; }
//never save hostile pets or pets left outside
if (c.TeamID == CharacterTeamType.None ||
c.TeamID == CharacterTeamType.Team2 ||
c.Submarine == null)
{
continue;
}
//pets must be in a player sub or owned by someone to be persistent
if (c.Submarine is not { Info.IsPlayer: true } &&
petBehavior.Owner is not { IsOnPlayerTeam: true })
{
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
{
WayPoint spawnPoint = null;
//try to find a spawnpoint in the main sub
if (Submarine.MainSub != null)
{
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 ?? Vector2.Zero;
}
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));
}
}
}
}
}