-
Notifications
You must be signed in to change notification settings - Fork 592
Expand file tree
/
Copy pathDamageableSystem.API.cs
More file actions
531 lines (451 loc) · 21 KB
/
Copy pathDamageableSystem.API.cs
File metadata and controls
531 lines (451 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
using System.Linq;
using Content.Shared._FarHorizons.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
#region Starlight
using Content.Shared._Starlight.Medical.Damage;
#endregion Starlight
namespace Content.Shared.Damage.Systems;
public sealed partial class DamageableSystem
{
/// <summary>
/// Directly sets the damage in a damageable component.
/// This method keeps the damage types supported by the DamageContainerPrototype in the component.
/// If a type is given in <paramref name="damage"/>, but not supported then it will not be set.
/// If a type is supported but not given in <paramref name="damage"/> then it will be set to 0.
/// </summary>
/// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised.
/// </remarks>
public void SetDamage(Entity<DamageableComponent?> ent, DamageSpecifier damage)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
foreach (var type in ent.Comp.Damage.DamageDict.Keys)
{
if (damage.DamageDict.TryGetValue(type, out var value))
ent.Comp.Damage.DamageDict[type] = value;
else
ent.Comp.Damage.DamageDict[type] = 0;
}
OnEntityDamageChanged((ent, ent.Comp));
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// This will overwrite the complete damage dict, meaning it will bulldoze the supported damage types.
/// </summary>
/// <remarks>
/// This may break persistance as the supported types are reset in case the component is initialized again.
/// So this only makes sense if you also change the DamageContainerPrototype in the component at the same time.
/// Only use this method if you know what you are doing.
/// </remarks>
public void SetDamageSpecifier(Entity<DamageableComponent?> ent, DamageSpecifier damage)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Damage = damage;
OnEntityDamageChanged((ent, ent.Comp));
}
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// If the attempt was successful or not.
/// </returns>
public bool TryChangeDamage(
Entity<DamageableComponent?> ent,
DamageSpecifier damage,
bool ignoreResistances = false,
bool interruptsDoAfters = true,
EntityUid? origin = null,
bool ignoreGlobalModifiers = false
)
#region Starlight
//! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
// If you deal 0.0 of some damage type, Empty will be false!
=> TryChangeDamage(ent, damage, out _, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
#endregion
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// If the attempt was successful or not.
/// </returns>
public bool TryChangeDamage(
Entity<DamageableComponent?> ent,
DamageSpecifier damage,
out DamageSpecifier newDamage,
bool ignoreResistances = false,
bool interruptsDoAfters = true,
EntityUid? origin = null,
bool ignoreGlobalModifiers = false
)
{
//! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
// If you deal 0.0 of some damage type, Empty will be false!
newDamage = ChangeDamage(ent, damage, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
return !newDamage.Empty;
}
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// The actual amount of damage taken, as a DamageSpecifier.
/// </returns>
public DamageSpecifier ChangeDamage(
Entity<DamageableComponent?> ent,
DamageSpecifier damage,
bool ignoreResistances = false,
bool interruptsDoAfters = true,
EntityUid? origin = null,
bool ignoreGlobalModifiers = false,
float armorPenetration = 0f, // 🌟Starlight🌟
bool canHeal = true // 🌟Starlight🌟
)
{
var damageDone = new DamageSpecifier();
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return damageDone;
if (damage.Empty)
return damageDone;
var before = new BeforeDamageChangedEvent(damage, origin);
RaiseLocalEvent(ent, ref before);
if (before.Cancelled)
return damageDone;
// Apply resistances
if (!ignoreResistances)
{
//Starlight Start
var computedModifiers = new DamageModifierSet();
if (
ent.Comp.DamageModifierSetId != null &&
_prototypeManager.Resolve(ent.Comp.DamageModifierSetId, out var modifierSet)
)
{
foreach (var coefficient in modifierSet.Coefficients)
computedModifiers.Coefficients.Add(coefficient.Key, modifierSet.Coefficients[coefficient.Key]);
foreach (var modifier in modifierSet.FlatReductions)
computedModifiers.FlatReductions.Add(modifier.Key, modifierSet.FlatReductions[modifier.Key]);
}
foreach(var additiveCoefficient in ent.Comp.AdditiveCoefficients)
if(computedModifiers.Coefficients.ContainsKey(additiveCoefficient.Key.ModifierKey))
computedModifiers.Coefficients[additiveCoefficient.Key.ModifierKey] += additiveCoefficient.Value;
else
computedModifiers.Coefficients.Add( additiveCoefficient.Key.ModifierKey, 1.0f + additiveCoefficient.Value);
foreach(var additiveModifier in ent.Comp.AdditiveModifiers)
if(computedModifiers.FlatReductions.ContainsKey(additiveModifier.Key.ModifierKey))
computedModifiers.FlatReductions[additiveModifier.Key.ModifierKey] += additiveModifier.Value;
else
computedModifiers.FlatReductions.Add( additiveModifier.Key.ModifierKey, 0.0f + additiveModifier.Value);
damage = DamageSpecifier.ApplyModifierSet(damage, computedModifiers, armorPenetration, canHeal);
//Starlight End
// TODO DAMAGE
// byref struct event.
var ev = new DamageModifyEvent(damage, origin, armorPenetration, canHeal); //starlight, added armor pen and heal flag
RaiseLocalEvent(ent, ev);
damage = ev.Damage;
if (damage.Empty)
return damageDone;
}
// Far Horizons start
// In a perfect world DamageModifyEvent would be used for that, but it's designed to not work when ignoreResistances is set to true, such as when healing system is doing it
if (damage.GetTotal() < 0)
{
var ev = new HealModifyEvent(damage, origin);
RaiseLocalEvent(ent, ev);
damage = ev.Damage;
}
// Far Horizons end
// 🌟Starlight🌟 start
var finalEv = new DamageBeforeApplyEvent
{
Damage = damage,
Origin = origin
};
RaiseLocalEvent(ent, finalEv);
if (finalEv.Cancelled)
return damage;
// 🌟Starlight🌟 end
if (!ignoreGlobalModifiers)
damage = ApplyUniversalAllModifiers(damage);
damageDone.DamageDict.EnsureCapacity(damage.DamageDict.Count);
var dict = ent.Comp.Damage.DamageDict;
foreach (var (type, value) in damage.DamageDict)
{
// CollectionsMarshal my beloved.
if (!dict.TryGetValue(type, out var oldValue))
continue;
var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
if (newValue == oldValue)
continue;
dict[type] = newValue;
damageDone.DamageDict[type] = newValue - oldValue;
}
if (!damageDone.Empty)
OnEntityDamageChanged((ent, ent.Comp), damageDone, interruptsDoAfters, origin);
return damageDone;
}
/// <summary>
/// Will reduce the damage on the entity exactly by <see cref="amount"/> as close as equally distributed among all damage types the entity has.
/// If one of the damage types of the entity is too low. it will heal that completly and distribute the excess healing among the other damage types.
/// If the <see cref="amount"/> is larger than the total damage of the entity then it just clears all damage.
/// </summary>
/// <param name="ent">entity to be healed</param>
/// <param name="amount">how much to heal. value has to be negative to heal</param>
/// <param name="group">from which group to heal. if null, heal from all groups</param>
/// <param name="origin">who did the healing</param>
public DamageSpecifier HealEvenly(
Entity<DamageableComponent?> ent,
FixedPoint2 amount,
ProtoId<DamageGroupPrototype>? group = null,
EntityUid? origin = null)
{
var damageChange = new DamageSpecifier();
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0)
return damageChange;
// Get our total damage, or heal if we're below a certain amount.
if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group))
return ChangeDamage(ent, -damage, true, false, origin);
// make sure damageChange has the same damage types as damage
damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count);
foreach (var type in damage.DamageDict.Keys)
{
damageChange.DamageDict.Add(type, FixedPoint2.Zero);
}
var remaining = -amount;
var keys = damage.DamageDict.Keys.ToList();
while (remaining > 0)
{
var count = keys.Count;
// We do this to ensure that we always round up when dividing to avoid excess loops.
// We already have logic to prevent healing more than we have.
var maxHeal = count == 1 ? remaining : (remaining + FixedPoint2.Epsilon * (count - 1)) / count;
// Iterate backwards since we're removing items.
for (var i = count - 1; i >= 0; i--)
{
var type = keys[i];
// This is the amount we're trying to heal, capped by maxHeal
var heal = damage.DamageDict[type] + damageChange.DamageDict[type];
// Don't go above max, if we don't go above max
if (heal > maxHeal)
heal = maxHeal;
// If we're not above max, we will heal it fully and don't need to enumerate anymore!
else
keys.RemoveAt(i);
if (heal >= remaining)
{
// Don't remove more than we can remove. Prevents us from healing more than we'd expect...
damageChange.DamageDict[type] -= remaining;
remaining = FixedPoint2.Zero;
break;
}
remaining -= heal;
damageChange.DamageDict[type] -= heal;
}
}
return ChangeDamage(ent, damageChange, true, false, origin);
}
/// <summary>
/// Will reduce the damage on the entity exactly by <see cref="amount"/> distributed by weight among all damage types the entity has.
/// (the weight is how much damage of the type there is)
/// If the <see cref="amount"/> is larger than the total damage of the entity then it just clears all damage.
/// </summary>
/// <param name="ent">entity to be healed</param>
/// <param name="amount">how much to heal. value has to be negative to heal</param>
/// <param name="group">from which group to heal. if null, heal from all groups</param>
/// <param name="origin">who did the healing</param>
public DamageSpecifier HealDistributed(
Entity<DamageableComponent?> ent,
FixedPoint2 amount,
ProtoId<DamageGroupPrototype>? group = null,
EntityUid? origin = null)
{
var damageChange = new DamageSpecifier();
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0)
return damageChange;
// Get our total damage, or heal if we're below a certain amount.
if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group))
return ChangeDamage(ent, -damage, true, false, origin);
// make sure damageChange has the same damage types as damageEntity
damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count);
var total = damage.GetTotal();
// heal weighted by the damage of that type
foreach (var (type, value) in damage.DamageDict)
{
damageChange.DamageDict.Add(type, value / total * amount);
}
return ChangeDamage(ent, damageChange, true, false, origin);
}
/// <summary>
/// Tries to get damage from an entity with an optional group specifier.
/// </summary>
/// <param name="ent">Entity we're checking the damage on</param>
/// <param name="amount">Amount we want the damage to be greater than ideally</param>
/// <param name="damage">Damage specifier we're returning with</param>
/// <param name="group">An optional group, note that if it fails to index it will just use all damage.</param>
/// <returns>True if the total damage is greater than the specified amount</returns>
public bool TryGetDamageGreaterThan(Entity<DamageableComponent> ent,
FixedPoint2 amount,
out DamageSpecifier damage,
ProtoId<DamageGroupPrototype>? group = null)
{
// get the damage should be healed (either all or only from one group)
damage = group == null ? GetDamage(ent) : GetDamage(ent, group.Value);
// If trying to heal more than the total damage of damageEntity just heal everything
return damage.GetTotal() > amount;
}
/// <summary>
/// Returns a <see cref="DamageSpecifier"/> with all positive damage of the entity from the group specified
/// </summary>
/// <param name="ent">entity with damage</param>
/// <param name="group">group of damage to get values from</param>
/// <returns></returns>
public DamageSpecifier GetDamage(Entity<DamageableComponent> ent, ProtoId<DamageGroupPrototype> group)
{
// No damage if no group exists...
if (!_prototypeManager.Resolve(group, out var groupProto))
return new DamageSpecifier();
var damage = new DamageSpecifier();
damage.DamageDict.EnsureCapacity(groupProto.DamageTypes.Count);
foreach (var damageId in groupProto.DamageTypes)
{
if (!ent.Comp.Damage.DamageDict.TryGetValue(damageId, out var value))
continue;
if (value > FixedPoint2.Zero)
damage.DamageDict.Add(damageId, value);
}
return damage;
}
/// <summary>
/// Returns a <see cref="DamageSpecifier"/> with all positive damage of the entity
/// </summary>
/// <param name="ent">entity with damage</param>
/// <returns></returns>
public DamageSpecifier GetDamage(Entity<DamageableComponent> ent)
{
var damage = new DamageSpecifier();
damage.DamageDict.EnsureCapacity(ent.Comp.Damage.DamageDict.Count);
foreach (var (damageId, value) in ent.Comp.Damage.DamageDict)
{
if (value > FixedPoint2.Zero)
damage.DamageDict.Add(damageId, value);
}
return damage;
}
/// <summary>
/// Applies the two universal "All" modifiers, if set.
/// Individual damage source modifiers are set in their respective code.
/// </summary>
/// <param name="damage">The damage to be changed.</param>
public DamageSpecifier ApplyUniversalAllModifiers(DamageSpecifier damage)
{
// Checks for changes first since they're unlikely in normal play.
if (
MathHelper.CloseToPercent(UniversalAllDamageModifier, 1f) &&
MathHelper.CloseToPercent(UniversalAllHealModifier, 1f)
)
return damage;
foreach (var (key, value) in damage.DamageDict)
{
if (value == 0)
continue;
if (value > 0)
{
damage.DamageDict[key] *= UniversalAllDamageModifier;
continue;
}
if (value < 0)
damage.DamageDict[key] *= UniversalAllHealModifier;
}
return damage;
}
#region Starlight
public void ClearAllDamage(Entity<DamageableComponent?> ent) =>
SetAllDamage(ent, FixedPoint2.Zero);
#endregion
/// <summary>
/// Sets all damage types supported by a <see cref="Components.DamageableComponent"/> to the specified value.
/// </summary>
/// <remarks>
/// Does nothing If the given damage value is negative.
/// </remarks>
public void SetAllDamage(Entity<DamageableComponent?> ent, FixedPoint2 newValue)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
if (newValue < 0)
return;
foreach (var type in ent.Comp.Damage.DamageDict.Keys)
{
ent.Comp.Damage.DamageDict[type] = newValue;
}
// Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
OnEntityDamageChanged((ent, ent.Comp), new DamageSpecifier());
}
/// <summary>
/// Set's the damage modifier set prototype for this entity.
/// </summary>
/// <param name="ent">The entity we're setting the modifier set of.</param>
/// <param name="damageModifierSetId">The prototype we're setting.</param>
public void SetDamageModifierSetId(Entity<DamageableComponent?> ent, ProtoId<DamageModifierSetPrototype>? damageModifierSetId)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.DamageModifierSetId = damageModifierSetId;
Dirty(ent);
}
// Begin Stellar - We need to be able to change DamageContainer to make cultists vulnerable to Holy Damage
public void SetDamageContainerID(Entity<DamageableComponent?> ent, ProtoId<DamageContainerPrototype>? damageContainerId)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.DamageContainerID = damageContainerId;
Dirty(ent);
}
// End Stellar
#region Starlight
/// <summary>
/// Adds to the additive damage modifiers of the DamageableComponent
/// </summary>
public void AddAdditiveModifierSet(Entity<DamageableComponent?> ent, EntityUid source, DamageModifierSet mods)
{
if(!Resolve(ent, ref ent.Comp))
return;
foreach (var coefficient in mods.Coefficients)
ent.Comp.AdditiveCoefficients.TryAdd((source, coefficient.Key), coefficient.Value);
foreach (var modifier in mods.FlatReductions)
ent.Comp.AdditiveModifiers.TryAdd((source, modifier.Key), modifier.Value);
}
/// <summary>
/// Subtracts from the additive damage modifiers of the DamageableComponent
/// </summary>
public void RemoveAdditiveModifierSet(Entity<DamageableComponent?> ent, EntityUid source, DamageModifierSet mods)
{
if(!Resolve(ent, ref ent.Comp))
return;
foreach (var coefficient in mods.Coefficients)
ent.Comp.AdditiveCoefficients.Remove((source, coefficient.Key));
foreach (var modifier in mods.FlatReductions)
ent.Comp.AdditiveModifiers.Remove((source, modifier.Key));
}
#endregion Starlight
}