using Content.Server.NPC.HTN;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Player;
namespace Content.Server.KillTracking;
///
/// This handles and recording who is damaging and killing entities.
///
public sealed class KillTrackingSystem : EntitySystem
{
///
public override void Initialize()
{
// Add damage to LifetimeDamage before MobStateChangedEvent gets raised
SubscribeLocalEvent(OnDamageChanged, before: [ typeof(MobThresholdSystem) ]);
SubscribeLocalEvent(OnMobStateChanged);
}
private void OnDamageChanged(EntityUid uid, KillTrackerComponent component, DamageChangedEvent args)
{
if (args.DamageDelta == null)
return;
if (!args.DamageIncreased)
{
foreach (var key in component.LifetimeDamage.Keys)
{
component.LifetimeDamage[key] -= args.DamageDelta.GetTotal();
}
return;
}
var source = GetKillSource(args.Origin);
var damage = component.LifetimeDamage.GetValueOrDefault(source);
component.LifetimeDamage[source] = damage + args.DamageDelta.GetTotal();
}
private void OnMobStateChanged(EntityUid uid, KillTrackerComponent component, MobStateChangedEvent args)
{
if (args.NewMobState != component.KillState || args.OldMobState >= args.NewMobState)
return;
// impulse is the entity that did the finishing blow.
var killImpulse = GetKillSource(args.Origin);
// source is the kill tracker source with the most damage dealt.
var largestSource = GetLargestSource(component.LifetimeDamage);
largestSource ??= killImpulse;
KillSource killSource;
KillSource? assistSource = null;
if (killImpulse is KillEnvironmentSource)
{
// if the kill was environmental, whatever did the most damage gets the kill.
killSource = largestSource;
}
else if (killImpulse == largestSource)
{
// if the impulse and the source are the same, there's no assist
killSource = killImpulse;
}
else
{
// the impulse gets the kill and the most damage gets the assist
killSource = killImpulse;
// no assist is given to environmental kills
if (largestSource is not KillEnvironmentSource
&& component.LifetimeDamage.TryGetValue(largestSource, out var largestDamage))
{
var killDamage = component.LifetimeDamage.GetValueOrDefault(killSource);
// you have to do at least twice as much damage as the killing source to get the assist.
if (largestDamage >= killDamage / 2)
assistSource = largestSource;
}
}
// it's a suicide if:
// - you caused your own death
// - the kill source was the entity that died
// - the entity that died had an assist on themselves
var suicide = args.Origin == uid ||
killSource is KillNpcSource npc && npc.NpcEnt == uid ||
killSource is KillPlayerSource player && player.PlayerId == CompOrNull(uid)?.PlayerSession.UserId ||
assistSource is KillNpcSource assistNpc && assistNpc.NpcEnt == uid ||
assistSource is KillPlayerSource assistPlayer && assistPlayer.PlayerId == CompOrNull(uid)?.PlayerSession.UserId;
var ev = new KillReportedEvent(uid, killSource, assistSource, suicide);
RaiseLocalEvent(uid, ref ev, true);
}
private KillSource GetKillSource(EntityUid? sourceEntity)
{
if (TryComp(sourceEntity, out var actor))
return new KillPlayerSource(actor.PlayerSession.UserId);
if (HasComp(sourceEntity))
return new KillNpcSource(sourceEntity.Value);
return new KillEnvironmentSource();
}
private KillSource? GetLargestSource(Dictionary lifetimeDamages)
{
KillSource? maxSource = null;
var maxDamage = FixedPoint2.Zero;
foreach (var (source, damage) in lifetimeDamages)
{
if (damage < maxDamage)
continue;
maxSource = source;
maxDamage = damage;
}
return maxSource;
}
}
///
/// Event broadcasted and raised by-ref on an entity with when they are killed.
///
/// The entity that was killed
/// The primary source of the kill
/// A secondary source of the kill. Can be null.
/// True if the entity that was killed caused their own death.
[ByRefEvent]
public readonly record struct KillReportedEvent(EntityUid Entity, KillSource Primary, KillSource? Assist, bool Suicide);