Client LuaCsForBarotrauma
PetBehavior.cs
4 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Xml.Linq;
9 using static Barotrauma.CharacterParams;
10 
11 namespace Barotrauma
12 {
14  {
15  public enum StatusIndicatorType
16  {
17  None,
18  Happy,
19  Sad,
20  Hungry
21  }
22 
23  private float hunger = 50.0f;
24  public float Hunger
25  {
26  get { return hunger; }
27  set { hunger = MathHelper.Clamp(value, 0.0f, MaxHunger); }
28  }
29 
30  private float happiness = 50.0f;
31  public float Happiness
32  {
33  get { return happiness; }
34  set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); }
35  }
36 
40  public float UnhappyThreshold { get; set; }
41 
45  public float HappyThreshold { get; set; }
46 
47  public float MaxHappiness { get; set; }
48 
49  public bool HideStatusIndicators { get; set; }
50 
54  public float HungryThreshold { get; set; }
55  public float MaxHunger { get; set; }
56 
57  public float HappinessDecreaseRate { get; set; }
58  public float HungerIncreaseRate { get; set; }
59 
60  public float PlayForce { get; set; }
61 
62  public float PlayTimer { get; set; }
63 
64  public float PlayCooldown { get; set; }
65 
69  public bool ToggleOwner { get; set; }
70 
71  private float? UnstunY { get; set; }
72 
73  public EnemyAIController AIController { get; private set; } = null;
74 
75  public Character Owner { get; set; }
76 
77  private class ItemProduction
78  {
79  public struct Item
80  {
82  public float Commonness;
83  }
84  public List<Item> Items;
85  public Vector2 HungerRange;
86  public Vector2 HappinessRange;
87  public float Rate;
88  public float HungerRate;
89  public float InvHungerRate;
90  public float HappinessRate;
91  public float InvHappinessRate;
92 
93  private readonly float totalCommonness;
94  private float timer;
95 
96  public ItemProduction(XElement element)
97  {
98  Items = new List<Item>();
99 
100  HungerRate = element.GetAttributeFloat("hungerrate", 0.0f);
101  InvHungerRate = element.GetAttributeFloat("invhungerrate", 0.0f);
102  HappinessRate = element.GetAttributeFloat("happinessrate", 0.0f);
103  InvHappinessRate = element.GetAttributeFloat("invhappinessrate", 0.0f);
104 
105  string[] requiredHappinessStr = element.GetAttributeString("requiredhappiness", "0-100").Split('-');
106  string[] requiredHungerStr = element.GetAttributeString("requiredhunger", "0-100").Split('-');
107  HappinessRange = new Vector2(0, 100);
108  HungerRange = new Vector2(0, 100);
109  float tempF;
110  if (requiredHappinessStr.Length >= 2)
111  {
112  if (float.TryParse(requiredHappinessStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.X = tempF; }
113  if (float.TryParse(requiredHappinessStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.Y = tempF; }
114  }
115  if (requiredHungerStr.Length >= 2)
116  {
117  if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.X = tempF; }
118  if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.Y = tempF; }
119  }
120  Rate = element.GetAttributeFloat("rate", 0.016f);
121  totalCommonness = 0.0f;
122  foreach (var subElement in element.Elements())
123  {
124  switch (subElement.Name.LocalName.ToLowerInvariant())
125  {
126  case "item":
127  Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty);
128  Item newItemToProduce = new Item
129  {
130  Prefab = identifier.IsEmpty ? null : ItemPrefab.Find("", subElement.GetAttributeIdentifier("identifier", Identifier.Empty)),
131  Commonness = subElement.GetAttributeFloat("commonness", 0.0f)
132  };
133  totalCommonness += newItemToProduce.Commonness;
134  Items.Add(newItemToProduce);
135  break;
136  }
137  }
138 
139  timer = 1.0f;
140  }
141 
142  public void Update(PetBehavior pet, float deltaTime)
143  {
144  if (pet.Happiness < HappinessRange.X || pet.Happiness > HappinessRange.Y) { return; }
145  if (pet.Hunger < HungerRange.X || pet.Hunger > HungerRange.Y) { return; }
146 
147  float currentRate = Rate;
148  currentRate += HappinessRate * (pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X);
149  currentRate += InvHappinessRate * (1.0f - ((pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X)));
150  currentRate += HungerRate * (pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X);
151  currentRate += InvHungerRate * (1.0f - ((pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X)));
152  timer -= currentRate * deltaTime;
153  if (timer <= 0.0f)
154  {
155  timer = 1.0f;
156  float r = Rand.Range(0.0f, totalCommonness);
157  float aggregate = 0.0f;
158  for (int i = 0; i < Items.Count; i++)
159  {
160  aggregate += Items[i].Commonness;
161  if (aggregate >= r && Items[i].Prefab != null)
162  {
163  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
164  Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
165  break;
166  }
167  }
168  }
169  }
170  }
171 
172  private class Food
173  {
174  public Identifier Tag;
175  public Vector2 HungerRange;
176  public float Hunger;
177  public float Happiness;
178  public float Priority;
179  public bool IgnoreContained;
180 
181  public CharacterParams.TargetParams TargetParams = null;
182  }
183 
184  private readonly List<ItemProduction> itemsToProduce = new List<ItemProduction>();
185  private readonly List<Food> foods = new List<Food>();
186 
187  public PetBehavior(XElement element, EnemyAIController aiController)
188  {
189  AIController = aiController;
190 
191  MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
192  UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
193  HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f);
194  HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false);
195 
196  MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f);
197  HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f);
198 
199  Happiness = MaxHappiness * 0.5f;
200  Hunger = MaxHunger * 0.5f;
201 
202  HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f);
203  HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f);
204 
205  PlayForce = element.GetAttributeFloat(nameof(PlayForce), 15.0f);
206  PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f);
207  ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false);
208 
209  foreach (var subElement in element.Elements())
210  {
211  switch (subElement.Name.LocalName.ToLowerInvariant())
212  {
213  case "itemproduction":
214  itemsToProduce.Add(new ItemProduction(subElement));
215  break;
216  case "eat":
217  Food food = new Food
218  {
219  Tag = subElement.GetAttributeIdentifier("tag", Identifier.Empty),
220  Hunger = subElement.GetAttributeFloat("hunger", -1),
221  Happiness = subElement.GetAttributeFloat("happiness", 1),
222  Priority = subElement.GetAttributeFloat("priority", 100),
223  IgnoreContained = subElement.GetAttributeBool("ignorecontained", true)
224  };
225  string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-');
226  food.HungerRange = new Vector2(0, 100);
227  if (requiredHungerStr.Length >= 2)
228  {
229  if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; }
230  if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; }
231  }
232  foods.Add(food);
233  break;
234  }
235  }
236 
237  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName);
238  }
239 
241  {
242  if (HideStatusIndicators) { return StatusIndicatorType.None; }
243  if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; }
244  if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; }
245  if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; }
246  return StatusIndicatorType.None;
247  }
248 
249  public bool OnEat(Item item)
250  {
251  bool success = OnEat(item.GetTags());
252  if (success)
253  {
254  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier);
255  }
256  return success;
257  }
258 
259  public bool OnEat(Character character)
260  {
261  if (character == null || !character.IsDead) { return false; }
262  bool success = OnEat("dead".ToIdentifier());
263  if (success)
264  {
265  GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName);
266  }
267  return success;
268  }
269 
270  private bool OnEat(IEnumerable<Identifier> tags)
271  {
272  foreach (Identifier tag in tags)
273  {
274  if (OnEat(tag)) { return true; }
275  }
276  return false;
277  }
278 
279  public bool OnEat(Identifier tag)
280  {
281  for (int i = 0; i < foods.Count; i++)
282  {
283  if (tag == foods[i].Tag)
284  {
285  Hunger += foods[i].Hunger;
286  Happiness += foods[i].Happiness;
287 #if CLIENT
289 #endif
290  return true;
291  }
292  }
293  return false;
294  }
295 
296  public void Play(Character player)
297  {
298  if (PlayTimer > 0.0f) { return; }
299  if (!AIController.Character.IsFriendly(player)) { return; }
300  if (ToggleOwner)
301  {
302  Owner = Owner == player ? null : player;
303  }
304  else
305  {
306  Owner ??= player;
307  }
310  Happiness += 10.0f;
312  UnstunY = AIController.Character.SimPosition.Y;
313 #if CLIENT
315 #endif
316  }
317 
318  public string GetTagName()
319  {
320  if (AIController.Character.Inventory != null)
321  {
322  foreach (Item item in AIController.Character.Inventory.AllItems)
323  {
324  var tag = item.GetComponent<NameTag>();
325  if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
326  {
327  return tag.WrittenName;
328  }
329  }
330  }
331 
332  return string.Empty;
333  }
334 
335  public void Update(float deltaTime)
336  {
337  var character = AIController.Character;
338  if (character?.Removed ?? true || character.IsDead) { return; }
339 
340  if (UnstunY.HasValue)
341  {
342  if (PlayTimer > PlayCooldown - 1.0f)
343  {
344  float extent = character.AnimController.MainLimb.body.GetMaxExtent();
345  if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) &&
346  character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f)
347  {
348  character.IsRagdolled = false;
349  UnstunY = null;
350  }
351  else
352  {
353  character.IsRagdolled = true;
354  }
355  }
356  else
357  {
358  character.IsRagdolled = false;
359  UnstunY = null;
360  }
361  }
362 
363  PlayTimer -= deltaTime;
364 
365  if (GameMain.NetworkMember?.IsClient ?? false) { return; }
366  if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; }
367 
368  Hunger += HungerIncreaseRate * deltaTime;
369  Happiness -= HappinessDecreaseRate * deltaTime;
370 
371  for (int i = 0; i < foods.Count; i++)
372  {
373  Food food = foods[i];
374  if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
375  {
376  if (food.TargetParams == null)
377  {
378  if (AIController.AIParams.TryGetTargets(food.Tag, out IEnumerable<TargetParams> existingTargetParams))
379  {
380  foreach (var targetParams in existingTargetParams)
381  {
382  food.TargetParams = targetParams;
383  }
384  }
385  else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
386  {
387  food.TargetParams = targetParams;
388  }
389  if (food.TargetParams != null)
390  {
391  food.TargetParams.State = AIState.Eat;
392  food.TargetParams.Priority = food.Priority;
393  food.TargetParams.IgnoreContained = food.IgnoreContained;
394  }
395  }
396  }
397  else if (food.TargetParams != null)
398  {
399  AIController.AIParams.RemoveTarget(food.TargetParams);
400  food.TargetParams = null;
401  }
402  }
403 
404  if (Hunger >= MaxHunger * 0.99f)
405  {
406  character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime));
407  }
408 
409  if (character.SelectedBy != null)
410  {
411  character.IsRagdolled = true;
412  UnstunY = character.SimPosition.Y;
413  }
414 
415  for (int i = 0; i < itemsToProduce.Count; i++)
416  {
417  itemsToProduce[i].Update(this, deltaTime);
418  }
419  }
420 
421  public static void SavePets(XElement petsElement)
422  {
423  foreach (Character c in Character.CharacterList)
424  {
425  if (!c.IsPet || c.IsDead) { continue; }
426  if (c.Submarine == null) { continue; }
427 
428  var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior;
429  if (petBehavior == null) { continue; }
430 
431  XElement petElement = new XElement("pet",
432  new XAttribute("speciesname", c.SpeciesName),
433  new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0),
434  new XAttribute("seed", c.Seed));
435 
436  var petBehaviorElement = new XElement("petbehavior",
437  new XAttribute("hunger", petBehavior.Hunger.ToString("G", CultureInfo.InvariantCulture)),
438  new XAttribute("happiness", petBehavior.Happiness.ToString("G", CultureInfo.InvariantCulture)));
439  petElement.Add(petBehaviorElement);
440 
441  var healthElement = new XElement("health");
442  c.CharacterHealth.Save(healthElement);
443  petElement.Add(healthElement);
444 
445  if (c.Inventory != null)
446  {
447  var inventoryElement = new XElement("inventory");
448  Character.SaveInventory(c.Inventory, inventoryElement);
449  petElement.Add(inventoryElement);
450  }
451 
452  petsElement.Add(petElement);
453  }
454  }
455 
456  public static void LoadPets(XElement petsElement)
457  {
458  foreach (var subElement in petsElement.Elements())
459  {
460  string speciesName = subElement.GetAttributeString("speciesname", "");
461  string seed = subElement.GetAttributeString("seed", "123");
462  int ownerHash = subElement.GetAttributeInt("ownerhash", 0);
463  Vector2 spawnPos = Vector2.Zero;
464  Character owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == ownerHash);
465  if (owner != null && owner.Submarine?.Info.Type == SubmarineType.Player)
466  {
467  spawnPos = owner.WorldPosition;
468  }
469  else
470  {
471  WayPoint spawnPoint = null;
472  //try to find a spawnpoint in the main sub
473  if (Submarine.MainSub != null)
474  {
475  spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
476  }
477  //if not found, try any player sub (shuttle/drone etc)
478  spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
479  spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Vector2.Zero;
480  }
481 
482  var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier());
483  if (characterPrefab == null)
484  {
485  DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found.");
486  continue;
487  }
488  var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false);
489  if (pet != null)
490  {
491  var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
492  if (petBehavior != null)
493  {
494  petBehavior.Owner = owner;
495  var petBehaviorElement = subElement.Element("petbehavior");
496  if (petBehaviorElement != null)
497  {
498  petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f);
499  petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f);
500  }
501  }
502  }
503  var inventoryElement = subElement.Element("inventory");
504  if (inventoryElement != null)
505  {
506  pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null));
507  }
508  }
509  }
510  }
511 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static AfflictionPrefab InternalDamage
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
static void SaveInventory(Inventory inventory, XElement parentElement)
bool IsFriendly(Character other)
void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor=1.0f, float maxInterval=0)
void SpawnInventoryItems(Inventory inventory, ContentXElement itemData)
int GetIdentifier()
Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name,...
Contains character data that should be editable in the character editor.
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
readonly Identifier Identifier
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
IReadOnlyCollection< Identifier > GetTags()
bool OnEat(Item item)
Definition: PetBehavior.cs:249
float HappyThreshold
At which point is the pet considered "happy" (playing happy sounds and showing the icon)
Definition: PetBehavior.cs:45
float UnhappyThreshold
At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon)
Definition: PetBehavior.cs:40
static void LoadPets(XElement petsElement)
Definition: PetBehavior.cs:456
bool OnEat(Identifier tag)
Definition: PetBehavior.cs:279
void Play(Character player)
Definition: PetBehavior.cs:296
void Update(float deltaTime)
Definition: PetBehavior.cs:335
static void SavePets(XElement petsElement)
Definition: PetBehavior.cs:421
float HungryThreshold
At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
Definition: PetBehavior.cs:54
bool OnEat(Character character)
Definition: PetBehavior.cs:259
StatusIndicatorType GetCurrentStatusIndicatorType()
Definition: PetBehavior.cs:240
PetBehavior(XElement element, EnemyAIController aiController)
Definition: PetBehavior.cs:187
bool ToggleOwner
Should the pet lose ownership (and stop following) when the same character interacts with it twice?...
Definition: PetBehavior.cs:69
readonly Identifier Identifier
Definition: Prefab.cs:34
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.