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