Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Content.Client/ADT/Weapons/Medbeam/ADTMedbeamSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Content.Shared.ADT.Weapons.Medbeam;

namespace Content.Client.ADT.Weapons.Medbeam;

public sealed class ADTMedbeamSystem : SharedADTMedbeamSystem
{
}
252 changes: 252 additions & 0 deletions Content.Server/ADT/Weapons/Medbeam/ADTMedbeamSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
using System.Linq;
using System.Numerics;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Mech.Systems;
using Content.Shared.ADT.Weapons.Medbeam;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Systems;
using Content.Shared.Mech.Components;
using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;

namespace Content.Server.ADT.Weapons.Medbeam;

public sealed class ADTMedbeamSystem : SharedADTMedbeamSystem
{
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SharedBloodstreamSystem _blood = default!;
[Dependency] private readonly ExplosionSystem _explosion = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly MechSystem _mech = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;

private readonly HashSet<EntityUid> _activeBeams = new();

public override void Update(float frameTime)
{
base.Update(frameTime);

if (_activeBeams.Count == 0)
return;

foreach (var uid in _activeBeams.ToArray())
{
if (!Exists(uid))
{
_activeBeams.Remove(uid);
continue;
}

var beam = Comp<ADTMedbeamComponent>(uid);

beam.Accumulator += frameTime;
if (beam.Accumulator < beam.UpdateInterval)
continue;

beam.Accumulator = 0;
TickBeam((uid, beam));
}
}

public override void AttachBeam(Entity<ADTMedbeamComponent> ent, EntityUid target)
{
base.AttachBeam(ent, target);
_activeBeams.Add(ent.Owner);
}

public override void DetachBeam(Entity<ADTMedbeamComponent> ent)
{
_activeBeams.Remove(ent.Owner);
base.DetachBeam(ent);
}

private void TickBeam(Entity<ADTMedbeamComponent> ent)
{
var target = ent.Comp.Target;
if (!Exists(target))
{
DetachBeam(ent);
return;
}

if (GetHolder(ent) is not { } holder)
{
DetachBeam(ent);
return;
}

var hasSomethingToHeal = HasSomethingToHeal(ent, target.Value);

if (TryComp<MechComponent>(holder, out var mech))
{
if (mech.PilotSlot.ContainedEntity is not { } pilot || !_mobState.IsAlive(pilot))
{
DetachBeam(ent);
return;
}

if (hasSomethingToHeal)
{
var energyUsage = (FixedPoint2) (ent.Comp.EnergyUsage * ent.Comp.UpdateInterval);
if (mech.Energy < energyUsage)
{
DetachBeam(ent);
return;
}

_mech.TryChangeEnergy(holder, -energyUsage, mech);
}
}
else if (ent.Comp.RequireMech || !_mobState.IsAlive(holder))
{
DetachBeam(ent);
return;
}

if (!_examine.InRangeUnOccluded(ent.Owner, target.Value, ent.Comp.MaxRange,
entity => entity == ent.Owner || entity == target.Value))
{
DetachBeam(ent);
return;
}

if (TryGetCrossing(ent, target.Value, out var otherGun, out var epicenter))
{
ExplodeBeams(ent, (otherGun, Comp<ADTMedbeamComponent>(otherGun)), epicenter);
return;
}

if (!hasSomethingToHeal)
return;

_damage.TryChangeDamage(target.Value, ent.Comp.Damage, origin: ent.Owner);

if (HasComp<BloodstreamComponent>(target.Value))
{
if (ent.Comp.BloodRestore > 0)
_blood.TryRegenerateBlood(target.Value, (FixedPoint2) ent.Comp.BloodRestore);

_blood.TryModifyBleedAmount(target.Value, -Comp<BloodstreamComponent>(target.Value).BleedAmount);
}
}

private bool HasSomethingToHeal(Entity<ADTMedbeamComponent> ent, EntityUid target)
{
if (TryComp<DamageableComponent>(target, out var damageable) && damageable.TotalDamage > 0)
return true;

if (HasComp<BloodstreamComponent>(target))
{
if (Comp<BloodstreamComponent>(target).BleedAmount > 0)
return true;

if (ent.Comp.BloodRestore > 0 && _blood.GetBloodLevel(target) < 1f)
return true;
}

return false;
}

private bool TryGetCrossing(Entity<ADTMedbeamComponent> ent, EntityUid target, out EntityUid otherGun, out MapCoordinates epicenter)
{
otherGun = default;
epicenter = default;

var gunCoords = _xform.GetMapCoordinates(ent.Owner);
var targetPos = _xform.GetMapCoordinates(target).Position;

var query = EntityQueryEnumerator<ADTMedbeamComponent>();
while (query.MoveNext(out var uid, out var other))
{
if (uid == ent.Owner || other.Target == null)
continue;

var otherCoords = _xform.GetMapCoordinates(uid);
var otherTargetCoords = _xform.GetMapCoordinates(other.Target.Value);
if (otherCoords.MapId != gunCoords.MapId || otherTargetCoords.MapId != gunCoords.MapId)
continue;

if (!TrySegmentIntersect(gunCoords.Position, targetPos, otherCoords.Position, otherTargetCoords.Position, out var point))
continue;

otherGun = uid;
epicenter = new MapCoordinates(point, gunCoords.MapId);
return true;
}

return false;
}

private void ExplodeBeams(Entity<ADTMedbeamComponent> a, Entity<ADTMedbeamComponent> b, MapCoordinates epicenter)
{
a.Comp.Target = null;
a.Comp.Accumulator = 0;
b.Comp.Target = null;
b.Comp.Accumulator = 0;
Dirty(a.Owner, a.Comp);
Dirty(b.Owner, b.Comp);

var explosionType = a.Comp.ExplosionType;
var totalIntensity = a.Comp.ExplosionTotalIntensity;
var slope = a.Comp.ExplosionIntensitySlope;
var maxTileIntensity = a.Comp.ExplosionMaxTileIntensity;

_explosion.QueueExplosion(epicenter, explosionType, totalIntensity, slope, maxTileIntensity, cause: a.Owner);
_explosion.QueueExplosion(_xform.GetMapCoordinates(a.Owner), explosionType, totalIntensity, slope, maxTileIntensity, cause: a.Owner);
_explosion.QueueExplosion(_xform.GetMapCoordinates(b.Owner), explosionType, totalIntensity, slope, maxTileIntensity, cause: b.Owner);

QueueDel(a.Owner);
QueueDel(b.Owner);
}

private static bool TrySegmentIntersect(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2, out Vector2 point)
{
point = default;

var r = a2 - a1;
var s = b2 - b1;
var rxs = Cross(r, s);
var qmp = b1 - a1;

if (MathHelper.CloseTo(rxs, 0))
{
if (!MathHelper.CloseTo(Cross(qmp, r), 0))
return false;

var denom = Vector2.Dot(r, r);
if (MathHelper.CloseTo(denom, 0))
return false;

var t0 = Vector2.Dot(qmp, r) / denom;
var t1 = t0 + Vector2.Dot(s, r) / denom;
var minT = MathF.Min(t0, t1);
var maxT = MathF.Max(t0, t1);

if (maxT < 0 || minT > 1)
return false;

point = a1 + r * Math.Clamp((minT + maxT) / 2, 0, 1);
return true;
}

var t = Cross(qmp, s) / rxs;
var u = Cross(qmp, r) / rxs;

if (t < 0 || t > 1 || u < 0 || u > 1)
return false;

point = a1 + r * t;
return true;
}

private static float Cross(Vector2 a, Vector2 b)
{
return a.X * b.Y - a.Y * b.X;
}
}
6 changes: 5 additions & 1 deletion Content.Shared/ADT/Mech/Systems/MechToolSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Content.Shared.ADT.Mech.Components;
using Content.Shared.ADT.Weapons.Medbeam;
using Content.Shared.Interaction;
using Content.Shared.Mech.Components;
using Content.Shared.Mech.Equipment.Components;
Expand Down Expand Up @@ -32,7 +33,10 @@ private void OnGetUsedEntity(EntityUid uid, MechComponent comp, ref GetUsedEntit
if (comp.Energy <= 0)
return;

if (comp.CurrentSelectedEquipment is not { } equipment || !HasComp<MechToolComponent>(equipment))
if (comp.CurrentSelectedEquipment is not { } equipment)
return;

if (!HasComp<MechToolComponent>(equipment) && !HasComp<ADTMedbeamComponent>(equipment))
Comment thread
CrimeMoot marked this conversation as resolved.
return;

args.Used = equipment;
Expand Down
86 changes: 86 additions & 0 deletions Content.Shared/ADT/Weapons/Medbeam/ADTMedbeamComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Numerics;
using Content.Shared.Damage;
using Content.Shared.Explosion;
using Content.Shared.Explosion.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Shared.ADT.Weapons.Medbeam;

[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ADTMedbeamComponent : Component
{
/// <summary>
/// The entity currently being healed by the beam.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Target;

/// <summary>
/// Whether the gun only works while installed inside a mech.
/// </summary>
[DataField]
public bool RequireMech;

/// <summary>
/// How much mech energy the beam drains per second while healing.
/// </summary>
[DataField]
public float EnergyUsage;

/// <summary>
/// How far the beam can stretch before it breaks.
/// </summary>
[DataField]
public float MaxRange = 8f;

/// <summary>
/// How often the beam heals the target.
/// </summary>
[DataField]
public float UpdateInterval = 1f;

[ViewVariables(VVAccess.ReadWrite)]
public float Accumulator;

/// <summary>
/// Healing applied per tick. Negative damage heals.
/// </summary>
[DataField]
public DamageSpecifier Damage = new();

/// <summary>
/// How much blood is restored per tick.
/// </summary>
[DataField]
public float BloodRestore = 5f;

/// <summary>
/// Explosion triggered when two beams cross.
/// </summary>
[DataField]
public ProtoId<ExplosionPrototype> ExplosionType = SharedExplosionSystem.DefaultExplosionPrototypeId;

[DataField]
public float ExplosionTotalIntensity = 100f;

[DataField]
public float ExplosionIntensitySlope = 4f;

[DataField]
public float ExplosionMaxTileIntensity = 10f;

[DataField]
public SpriteSpecifier Beam =
new SpriteSpecifier.Rsi(new ResPath("/Textures/ADT/Misc/medbeam.rsi"), "medbeam");

[DataField]
public SpriteSpecifier? Start;

[DataField]
public SpriteSpecifier? End;

[DataField]
public Vector2 Scale = Vector2.One;
}
Loading
Loading