Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs
3 using Barotrauma.Sounds;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 
9 namespace Barotrauma
10 {
11  partial class StatusEffect
12  {
13  private List<ParticleEmitter> particleEmitters;
14 
15  private readonly static HashSet<StatusEffect> ActiveLoopingSounds = new HashSet<StatusEffect>();
16  private static double LastMuffleCheckTime;
17  private readonly List<RoundSound> sounds = new List<RoundSound>();
18  public IEnumerable<RoundSound> Sounds { get { return sounds; } }
19  private SoundSelectionMode soundSelectionMode;
20  private SoundChannel soundChannel;
21  private Entity soundEmitter;
22  private double loopStartTime;
23  private bool loopSound;
30  private bool forcePlaySounds;
31 
32  private CoroutineHandle playSoundAfterLoadedCoroutine;
33 
34  partial void InitProjSpecific(ContentXElement element, string parentDebugName)
35  {
36  particleEmitters = new List<ParticleEmitter>();
37 
38  foreach (var subElement in element.Elements())
39  {
40  switch (subElement.Name.ToString().ToLowerInvariant())
41  {
42  case "particleemitter":
43  particleEmitters.Add(new ParticleEmitter(subElement));
44  break;
45  case "sound":
46  var sound = RoundSound.Load(subElement);
47  if (sound?.Sound != null)
48  {
49  loopSound = subElement.GetAttributeBool("loop", false);
50  if (subElement.GetAttribute("selectionmode") != null)
51  {
52  if (Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode))
53  {
54  soundSelectionMode = selectionMode;
55  }
56  }
57  sounds.Add(sound);
58  }
59  break;
60  }
61  }
62  forcePlaySounds = element.GetAttributeBool(nameof(forcePlaySounds), false);
63  }
64 
65  partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound)
66  {
67  if (playSound)
68  {
69  PlaySound(entity, hull, worldPosition);
70  }
71 
72  foreach (ParticleEmitter emitter in particleEmitters)
73  {
74  float angle = 0.0f;
75  float particleRotation = 0.0f;
76  bool mirrorAngle = false;
78  {
79  bool entityAngleAssigned = false;
80  Limb targetLimb = null;
81  if (entity is Item item && item.body != null)
82  {
83  angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
84  particleRotation = -item.body.Rotation;
85  if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f)
86  {
87  particleRotation += MathHelper.Pi;
88  mirrorAngle = true;
89  }
90  entityAngleAssigned = true;
91  }
92  if (emitter.Prefab.Properties.CopyTargetAngle || !entityAngleAssigned)
93  {
94  if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l)
95  {
96  targetLimb = c.AnimController.GetLimb(l);
97  }
98  else
99  {
100  for (int i = 0; i < targets.Count; i++)
101  {
102  if (targets[i] is Limb limb)
103  {
104  targetLimb = limb;
105  break;
106  }
107  }
108  }
109  }
110  if (targetLimb != null && !targetLimb.Removed)
111  {
112  angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
113  particleRotation = -targetLimb.body.Rotation;
114  float offset = targetLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
115  particleRotation += offset;
116  if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f)
117  {
118  angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
119  particleRotation = -targetLimb.body.Rotation;
120  if (targetLimb.body.Dir < 0.0f)
121  {
122  particleRotation += MathHelper.Pi;
123  mirrorAngle = true;
124  }
125  }
126  }
127  }
128 
129  emitter.Emit(deltaTime, worldPosition, hull, angle: angle, particleRotation: particleRotation, mirrorAngle: mirrorAngle);
130  }
131  }
132 
133  private bool ignoreMuffling;
134 
135  private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition)
136  {
137  if (sounds.Count == 0) { return; }
138 
139  if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds)
140  {
141  if (soundChannel != null && soundChannel.IsPlaying)
142  {
143  soundChannel.FadeOutAndDispose();
144  }
145  if (soundSelectionMode == SoundSelectionMode.All)
146  {
147  foreach (RoundSound sound in sounds)
148  {
149  if (sound?.Sound == null)
150  {
151  string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace();
152  GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
153  return;
154  }
155  PlaySoundOrDelayIfNotLoaded(sound);
156  }
157  }
158  else
159  {
160  int selectedSoundIndex;
161  if (soundSelectionMode == SoundSelectionMode.ItemSpecific && entity is Item item)
162  {
163  selectedSoundIndex = item.ID % sounds.Count;
164  }
165  else if (soundSelectionMode == SoundSelectionMode.CharacterSpecific && entity is Character user)
166  {
167  selectedSoundIndex = user.ID % sounds.Count;
168  }
169  else
170  {
171  selectedSoundIndex = Rand.Int(sounds.Count);
172  }
173  var selectedSound = sounds[selectedSoundIndex];
174  if (selectedSound?.Sound == null)
175  {
176  string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace();
177  GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
178  return;
179  }
180  PlaySoundOrDelayIfNotLoaded(selectedSound);
181  }
182  }
183  else
184  {
185  soundChannel.Position = new Vector3(worldPosition, 0.0f);
186  }
187 
188  if (soundChannel != null && soundChannel.Looping)
189  {
190  ActiveLoopingSounds.Add(this);
191  soundEmitter = entity;
192  loopStartTime = Timing.TotalTime;
193  }
194 
195  void PlaySoundOrDelayIfNotLoaded(RoundSound selectedSound)
196  {
197  if (playSoundAfterLoadedCoroutine != null) { return; }
198  if (selectedSound.Sound.Loading)
199  {
200  playSoundAfterLoadedCoroutine = CoroutineManager.StartCoroutine(PlaySoundAfterLoaded(selectedSound));
201  }
202  else
203  {
204  PlaySound(selectedSound);
205  }
206  }
207 
208  IEnumerable<CoroutineStatus> PlaySoundAfterLoaded(RoundSound selectedSound)
209  {
210  float maxWaitTimer = 1.0f;
211  while (selectedSound.Sound.Loading && maxWaitTimer > 0.0f)
212  {
213  maxWaitTimer -= CoroutineManager.DeltaTime;
214  yield return CoroutineStatus.Running;
215  }
216  if (!selectedSound.Sound.Loading)
217  {
218  PlaySound(selectedSound);
219  }
220  yield return CoroutineStatus.Success;
221  }
222 
223  void PlaySound(RoundSound selectedSound)
224  {
225  //if the sound loops, we must make sure the existing channel
226  System.Diagnostics.Debug.Assert(
227  soundChannel == null || !soundChannel.IsPlaying || soundChannel.FadingOutAndDisposing || !soundChannel.Looping,
228  "A StatusEffect attempted to play a sound, but an looping sound is already playing. The looping sound should be stopped before playing a new one, or it will keep looping indefinitely.");
229 
230  soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling, freqMult: selectedSound.GetRandomFrequencyMultiplier());
231  ignoreMuffling = selectedSound.IgnoreMuffling;
232  if (soundChannel != null) { soundChannel.Looping = loopSound; }
233  }
234  }
235 
236  static partial void UpdateAllProjSpecific(float deltaTime)
237  {
238  bool doMuffleCheck = Timing.TotalTime > LastMuffleCheckTime + 0.2;
239  if (doMuffleCheck) { LastMuffleCheckTime = Timing.TotalTime; }
240  foreach (StatusEffect statusEffect in ActiveLoopingSounds)
241  {
242  if (statusEffect.soundChannel == null) { continue; }
243 
244  //stop looping sounds if the statuseffect hasn't been applied in 0.1
245  //= keeping the sound looping requires continuously applying the statuseffect
246  if (Timing.TotalTime > statusEffect.loopStartTime + 0.1 && !DurationList.Any(e => e.Parent == statusEffect))
247  {
248  statusEffect.soundChannel.FadeOutAndDispose();
249  statusEffect.soundChannel = null;
250  }
251  else if (statusEffect.soundEmitter is { Removed: false })
252  {
253  statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f);
254  if (doMuffleCheck && !statusEffect.ignoreMuffling)
255  {
256  statusEffect.soundChannel.Muffled = SoundPlayer.ShouldMuffleSound(
257  Character.Controlled, statusEffect.soundEmitter.WorldPosition, statusEffect.soundChannel.Far, Character.Controlled?.CurrentHull);
258  }
259  }
260  }
261  ActiveLoopingSounds.RemoveWhere(s => s.soundChannel == null);
262  }
263  }
264 }
IEnumerable< ContentXElement > Elements()
bool GetAttributeBool(string key, bool def)
readonly ParticleEmitterPrefab Prefab
void Emit(float deltaTime, Vector2 position, Hull? hullGuess=null, float angle=0.0f, float particleRotation=0.0f, float velocityMultiplier=1.0f, float sizeMultiplier=1.0f, float amountMultiplier=1.0f, Color? colorMultiplier=null, ParticlePrefab? overrideParticle=null, bool mirrorAngle=false, Tuple< Vector2, Vector2 >? tracerPoints=null)
readonly ParticleEmitterProperties Properties
static ? RoundSound Load(ContentXElement element, bool stream=false)
Definition: RoundSound.cs:61
static readonly List< DurationListElement > DurationList
readonly LimbType[] targetLimbs
Which types of limbs this effect can target? Only valid when targeting characters or limbs.