Client LuaCsForBarotrauma
AfflictionHusk.cs
1 using System.Collections.Generic;
2 using System.Linq;
3 using System.Xml.Linq;
4 using System;
6 using Microsoft.Xna.Framework;
7 
8 namespace Barotrauma
9 {
14  partial class AfflictionHusk : Affliction
15  {
16  public enum InfectionState
17  {
18  Initial, Dormant, Transition, Active, Final
19  }
20 
21  private bool subscribedToDeathEvent;
22 
23  private InfectionState state;
24 
25  private List<Limb> huskAppendage;
26 
27  private Character character;
28 
29  private bool stun = false;
30 
32  public override float Strength
33  {
34  get { return _strength; }
35  set
36  {
37  // Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks.
38  float previousValue = _strength;
39  float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
40  float max = Math.Max(threshold, previousValue);
41  _strength = Math.Clamp(value, 0, max);
42  stun = GameMain.GameSession?.IsRunning ?? true;
43  if (previousValue > 0.0f && value <= 0.0f)
44  {
45  DeactivateHusk();
46  highestStrength = 0;
47  }
48  activeEffectDirty = true;
49  }
50  }
51  private float highestStrength;
52 
54  {
55  get { return state; }
56  private set
57  {
58  if (state == value) { return; }
59  state = value;
60  }
61  }
62 
64 
65  private float DormantThreshold => HuskPrefab.DormantThreshold;
66  private float ActiveThreshold => HuskPrefab.ActiveThreshold;
67  private float TransitionThreshold => HuskPrefab.TransitionThreshold;
68 
69  private float TransformThresholdOnDeath => HuskPrefab.TransformThresholdOnDeath;
70 
71  public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength)
72  {
73  HuskPrefab = prefab as AfflictionPrefabHusk;
74  if (HuskPrefab == null)
75  {
76  DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!",
77  contentPackage: prefab.ContentPackage);
78  }
79  }
80 
81  public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
82  {
83  if (HuskPrefab == null) { return; }
84  base.Update(characterHealth, targetLimb, deltaTime);
85  highestStrength = Math.Max(_strength, highestStrength);
86  character = characterHealth.Character;
87  if (character == null) { return; }
88 
89  UpdateMessages();
90 
91  if (!subscribedToDeathEvent)
92  {
93  character.OnDeath += CharacterDead;
94  subscribedToDeathEvent = true;
95  }
96  if (Strength < DormantThreshold)
97  {
98  DeactivateHusk();
99  if (Strength > Math.Min(1.0f, DormantThreshold))
100  {
101  State = InfectionState.Dormant;
102  }
103  }
104  else if (Strength < ActiveThreshold)
105  {
106  DeactivateHusk();
107  if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
108  {
109  character.SpeechImpediment = 30;
110  }
111  State = InfectionState.Transition;
112  }
113  else if (Strength < TransitionThreshold)
114  {
115  if (State != InfectionState.Active && stun)
116  {
117  character.SetStun(Rand.Range(2f, 3f));
118  }
119  if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
120  {
121  character.SpeechImpediment = 100;
122  }
123  State = InfectionState.Active;
124  ActivateHusk();
125  }
126  else
127  {
128  State = InfectionState.Final;
129  ActivateHusk();
130  ApplyDamage(deltaTime);
131  character.SetStun(5);
132  }
133  }
134 
135  private InfectionState? prevDisplayedMessage;
136  private void UpdateMessages()
137  {
138  if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; }
139  if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; }
140  if (highestStrength > Strength) { return; }
141 
142  // Show initial husk warning by default, and disable it only if campaign difficulty settings explicitly disable it
143  bool showHuskWarning = GameMain.GameSession?.Campaign?.Settings.ShowHuskWarning ?? true;
144 
145  switch (State)
146  {
147  case InfectionState.Dormant:
148  if (Strength < DormantThreshold * 0.5f)
149  {
150  return;
151  }
152  if (showHuskWarning)
153  {
154  if (character == Character.Controlled)
155  {
156 #if CLIENT
157  GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red);
158 #endif
159  }
160  else if (character.IsBot)
161  {
162  character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier());
163  }
164  }
165  break;
166  case InfectionState.Transition:
167  if (character == Character.Controlled)
168  {
169 #if CLIENT
170  GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red);
171 #endif
172  }
173  else if (character.IsBot)
174  {
175  character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier());
176  }
177  break;
178  case InfectionState.Active:
179 #if CLIENT
180  if (character == Character.Controlled && character.Params.UseHuskAppendage)
181  {
182  GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red);
183  }
184 #endif
185  break;
186  case InfectionState.Final:
187  default:
188  break;
189  }
190  prevDisplayedMessage = State;
191  }
192 
193  private const float DamageCooldown = 0.1f;
194  private float damageCooldownTimer;
195  private void ApplyDamage(float deltaTime)
196  {
197  if (damageCooldownTimer > 0)
198  {
199  damageCooldownTimer -= deltaTime;
200  return;
201  }
202  damageCooldownTimer = DamageCooldown;
203  int limbCount = character.AnimController.Limbs.Count(IsValidLimb);
204  foreach (Limb limb in character.AnimController.Limbs)
205  {
206  if (!IsValidLimb(limb)) { continue; }
207  float random = Rand.Value();
208  if (random == 0) { continue; }
209  const float damageRate = 2;
210  float dmg = random / limbCount * damageRate;
211  character.LastDamageSource = null;
212  var afflictions = AfflictionPrefab.InternalDamage.Instantiate(dmg).ToEnumerable();
213  const float forceMultiplier = 5;
214  float force = dmg * limb.Mass * forceMultiplier;
215  character.DamageLimb(limb.WorldPosition, limb, afflictions, stun: 0, playSound: false, Rand.Vector(force), ignoreDamageOverlay: true, recalculateVitality: false);
216  }
218 
219  static bool IsValidLimb(Limb limb) => !limb.IgnoreCollisions && !limb.IsSevered && !limb.Hidden;
220  }
221 
222  public void ActivateHusk()
223  {
224  if (huskAppendage == null && character.Params.UseHuskAppendage)
225  {
226  var huskAffliction = Prefab as AfflictionPrefabHusk;
227  huskAppendage = AttachHuskAppendage(character, huskAffliction, GetHuskedSpeciesName(character.Params, huskAffliction));
228  }
229 
230  if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
231  {
232  character.NeedsAir = false;
233  }
234 
235  if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
236  {
237  character.SpeechImpediment = 100;
238  }
239  }
240 
241  private void DeactivateHusk()
242  {
243  if (character?.AnimController == null || character.Removed) { return; }
244  if (Prefab is AfflictionPrefabHusk { NeedsAir: false } &&
245  !character.CharacterHealth.GetAllAfflictions().Any(a => a != this && a.Prefab is AfflictionPrefabHusk { NeedsAir: false }))
246  {
247  character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false);
248  }
249 
250  if (huskAppendage != null)
251  {
252  huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
253  huskAppendage = null;
254  }
255  }
256 
258  {
259  if (character == null || !subscribedToDeathEvent) { return; }
260  character.OnDeath -= CharacterDead;
261  subscribedToDeathEvent = false;
262  }
263 
264  private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
265  {
266  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
267  if (Strength < TransformThresholdOnDeath || character.Removed ||
268  character.CharacterHealth.GetAllAfflictions().Any(a => a.GetActiveEffect()?.BlockTransformation.Contains(Prefab.Identifier) ?? false))
269  {
271  return;
272  }
273 
274  //don't turn the character into a husk if any of its limbs are severed
275  if (character.AnimController?.LimbJoints != null)
276  {
277  foreach (var limbJoint in character.AnimController.LimbJoints)
278  {
279  if (limbJoint.IsSevered) { return; }
280  }
281  }
282 
283  //create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
284  CoroutineManager.StartCoroutine(CreateAIHusk());
285  }
286 
287  private IEnumerable<CoroutineStatus> CreateAIHusk()
288  {
289  //character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
290  // -> don't spawn the AI husk
291  if (Entity.Spawner.IsInRemoveQueue(character))
292  {
293  yield return CoroutineStatus.Success;
294  }
295 #if SERVER
296  var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
297 #endif
298  character.Enabled = false;
299  Entity.Spawner.AddEntityToRemoveQueue(character);
301 
302  Identifier huskedSpeciesName = GetHuskedSpeciesName(character.Params, Prefab as AfflictionPrefabHusk);
303  CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
304 
305  if (prefab == null)
306  {
307  DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.",
308  contentPackage: Prefab.ContentPackage);
309  yield return CoroutineStatus.Success;
310  }
311 
312  XElement parentElement = new XElement("CharacterInfo");
313  XElement infoElement = character.Info?.Save(parentElement);
314  CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(new ContentXElement(Prefab.ContentPackage, infoElement));
315 
316  if (huskCharacterInfo != null)
317  {
318  var bodyTint = GetBodyTint();
319  huskCharacterInfo.Head.SkinColor =
320  Color.Lerp(huskCharacterInfo.Head.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f);
321  }
322 
323  var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true);
324  if (husk.Info != null)
325  {
326  husk.Info.Character = husk;
327  husk.Info.TeamID = CharacterTeamType.None;
328  }
329  husk.AllowPlayDead = character.AllowPlayDead;
330 
331  if (Prefab is AfflictionPrefabHusk huskPrefab)
332  {
333  if (huskPrefab.ControlHusk || GameMain.LuaCs.Game.enableControlHusk)
334  {
335 #if SERVER
336  if (client != null)
337  {
338  GameMain.Server.SetClientCharacter(client, husk);
339  GameMain.LuaCs.Hook.Call("husk.clientControlHusk", new object[] { client, husk });
340  }
341 #else
342  if (!character.IsRemotelyControlled && character == Character.Controlled)
343  {
344  Character.Controlled = husk;
345  }
346 #endif
347  }
348  }
349 
350  foreach (Limb limb in husk.AnimController.Limbs)
351  {
352  if (limb.type == LimbType.None)
353  {
354  limb.body.SetTransform(character.SimPosition, 0.0f);
355  continue;
356  }
357 
358  var matchingLimb = character.AnimController.GetLimb(limb.type);
359  if (matchingLimb?.body != null)
360  {
361  limb.body.SetTransform(matchingLimb.SimPosition, matchingLimb.Rotation);
362  limb.body.LinearVelocity = matchingLimb.LinearVelocity;
363  limb.body.AngularVelocity = matchingLimb.body.AngularVelocity;
364  }
365  }
366 
367  if ((Prefab as AfflictionPrefabHusk)?.TransferBuffs ?? false)
368  {
369  foreach (Affliction affliction in character.CharacterHealth.GetAllAfflictions())
370  {
371  if (affliction.Prefab.IsBuff)
372  {
373  husk.CharacterHealth.ApplyAffliction(
374  character.CharacterHealth.GetAfflictionLimb(affliction),
375  affliction.Prefab.Instantiate(affliction.Strength));
376  }
377  }
378  }
379 
380  if (character.Inventory != null && husk.Inventory != null)
381  {
382  for (int i = 0; i < character.Inventory.Capacity && i < husk.Inventory.Capacity; i++)
383  {
384  character.Inventory.GetItemsAt(i).ForEachMod(item => husk.Inventory.TryPutItem(item, i, true, false, null));
385  }
386  }
387 
388  husk.SetStun(5);
389  yield return new WaitForSeconds(5, false);
390 #if CLIENT
391  husk?.PlaySound(CharacterSound.SoundType.Idle);
392 #endif
393  yield return CoroutineStatus.Success;
394  }
395 
396  public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
397  {
398  var appendage = new List<Limb>();
399  CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
400  if (huskPrefab?.ConfigElement == null)
401  {
402  DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!",
403  contentPackage: matchingAffliction.ContentPackage);
404  return appendage;
405  }
406  var mainElement = huskPrefab.ConfigElement;
407  var element = appendageDefinition;
408  if (element == null)
409  {
410  element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == matchingAffliction.Identifier);
411  }
412  if (element == null)
413  {
414  DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!",
415  contentPackage: matchingAffliction.ContentPackage);
416  return appendage;
417  }
418  ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
419  XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
420  if (doc == null) { return appendage; }
421  ragdoll ??= character.AnimController;
422  if (ragdoll.Dir < 1.0f)
423  {
424  ragdoll.Flip();
425  }
426 
427  var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
428  var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
429  //the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
430  //that take up the IDs of this appendage
431  int idOffset = 0;
432  foreach (var jointElement in root.GetChildElements("joint"))
433  {
434  if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
435 
436  var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
437  Limb attachLimb = null;
438  if (matchingAffliction.AttachLimbId > -1)
439  {
440  attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
441  }
442  else if (matchingAffliction.AttachLimbName != null)
443  {
444  attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
445  }
446  else if (matchingAffliction.AttachLimbType != LimbType.None)
447  {
448  attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
449  }
450  if (attachLimb == null)
451  {
452  attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
453  }
454  if (attachLimb != null)
455  {
456  jointParams.Limb1 = attachLimb.Params.ID;
457  //the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
458  // -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
459  if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
460  {
461  jointParams.Limb1 += idOffset;
462  }
463  var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
464  if (idOffset == 0)
465  {
466  idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
467  }
468  jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
469  Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
470  huskAppendage.body.Submarine = character.Submarine;
471  huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
472  ragdoll.AddLimb(huskAppendage);
473  ragdoll.AddJoint(jointParams);
474  appendage.Add(huskAppendage);
475  }
476  }
477  return appendage;
478  }
479 
481  {
482  Identifier huskedSpecies = character.HuskedSpecies;
483  if (huskedSpecies.IsEmpty)
484  {
485  // Default pattern: Crawler -> Crawlerhusk, Human -> Humanhusk
486  return new Identifier(character.SpeciesName.Value + prefab.HuskedSpeciesName.Value);
487  }
488  return huskedSpecies;
489  }
490 
492  {
493  Identifier nonHuskedSpecies = character.NonHuskedSpecies;
494  if (nonHuskedSpecies.IsEmpty)
495  {
496  // Default pattern: Crawlerhusk -> Crawler, Humanhusk -> Human
497  return character.SpeciesName.Remove(prefab.HuskedSpeciesName);
498  }
499  return nonHuskedSpecies;
500  }
501  }
502 }
Identifier Identifier
Definition: Affliction.cs:62
void Serialize(XElement element)
Definition: Affliction.cs:125
Affliction(AfflictionPrefab prefab, float strength)
Definition: Affliction.cs:97
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
A special affliction type that gradually makes the character turn into another type of character....
readonly AfflictionPrefabHusk HuskPrefab
static List< Limb > AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition=null, Ragdoll ragdoll=null)
override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
AfflictionHusk(AfflictionPrefab prefab, float strength)
override float???? Strength
static Identifier GetNonHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
static Identifier GetHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
AfflictionPrefabHusk is a special type of affliction that has added functionality for husk infection.
readonly float TransformThresholdOnDeath
The minimum strength the affliction must have for the affected character to transform into a husk upo...
readonly float DormantThreshold
The minimum strength at which husk infection will be in the dormant stage. It must be less than or eq...
readonly float TransitionThreshold
The minimum strength at which husk infection will be in its final stage. It must be greater than or e...
readonly Identifier HuskedSpeciesName
The species of husk to convert the affected character to once husk infection reaches its final stage.
readonly float ActiveThreshold
The minimum strength at which husk infection will be in the active stage. It must be greater than or ...
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
bool IsRemotelyControlled
Is the character controlled remotely (either by another player, or a server-side AIController)
void SetStun(float newStun, bool allowStunDecrease=false, bool isNetworkMessage=false)
AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable< Affliction > afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker=null, float damageMultiplier=1, bool allowStacking=true, float penetration=0f, bool shouldImplode=false, bool ignoreDamageOverlay=false, bool recalculateVitality=true)
Contains character data that should be editable in the character editor.
override ContentXElement? MainElement
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
readonly? ContentPackage ContentPackage
Definition: ContentPath.cs:21
static readonly ContentPath Empty
Definition: ContentPath.cs:12
IEnumerable< ContentXElement > GetChildElements(string name)
bool GetAttributeBool(string key, bool def)
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
Inventory(Entity owner, int capacity, int slotsPerRow=5)
IEnumerable< Item > GetItemsAt(int index)
Get all the item stored in the specified inventory slot. Can return more than one item if the slot co...
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
Limb GetLimb(LimbType limbType, bool excludeSevered=true, bool excludeLimbsWithSecondaryType=false, bool useSecondaryType=false)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
@ Character
Characters only