Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Psionic "Heal Other" Powers #942

Merged
merged 15 commits into from
Sep 21, 2024
152 changes: 152 additions & 0 deletions Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Robust.Shared.Player;
using Content.Server.DoAfter;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Popups;
using Content.Shared.Psionics.Events;
using Content.Shared.Examine;
using static Content.Shared.Examine.ExamineSystemShared;
using Robust.Shared.Timing;
using Content.Shared.Actions.Events;
using Robust.Server.Audio;
using Content.Server.Atmos.Rotting;
using Content.Shared.Mobs.Systems;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Psionics.Glimmer;

namespace Content.Server.Abilities.Psionics;

public sealed class RevivifyPowerSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly RottingSystem _rotting = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly GlimmerSystem _glimmer = default!;

VMSolidus marked this conversation as resolved.
Show resolved Hide resolved

public override void Initialize()
{
base.Initialize();
VMSolidus marked this conversation as resolved.
Show resolved Hide resolved

SubscribeLocalEvent<PsionicComponent, PsionicHealOtherPowerActionEvent>(OnPowerUsed);
SubscribeLocalEvent<PsionicComponent, DispelledEvent>(OnDispelled);
SubscribeLocalEvent<PsionicComponent, PsionicHealOtherDoAfterEvent>(OnDoAfter);
}

VMSolidus marked this conversation as resolved.
Show resolved Hide resolved

private void OnPowerUsed(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
{
if (component.DoAfter is not null)
return;

if (!args.Immediate)
AttemptDoAfter(uid, component, args);
else ActivatePower(uid, component, args);

if (args.PopupText is not null
&& _glimmer.Glimmer > args.GlimmerObviousPopupThreshold * component.CurrentDampening)
_popupSystem.PopupEntity(Loc.GetString(args.PopupText, ("entity", uid)), uid,
Filter.Pvs(uid).RemoveWhereAttachedEntity(entity => !_examine.InRangeUnOccluded(uid, entity, ExamineRange, null)),
true,
args.PopupType);

if (args.PlaySound
&& _glimmer.Glimmer > args.GlimmerObviousSoundThreshold * component.CurrentDampening)
_audioSystem.PlayPvs(args.SoundUse, uid, args.AudioParams);

// Sanitize the Glimmer inputs because otherwise the game will crash if someone makes MaxGlimmer lower than MinGlimmer.
var minGlimmer = (int) Math.Round(MathF.MinMagnitude(args.MinGlimmer, args.MaxGlimmer)
+ component.CurrentAmplification - component.CurrentDampening);
var maxGlimmer = (int) Math.Round(MathF.MaxMagnitude(args.MinGlimmer, args.MaxGlimmer)
+ component.CurrentAmplification - component.CurrentDampening);

_psionics.LogPowerUsed(uid, args.PowerName, minGlimmer, maxGlimmer);
args.Handled = true;
}

private void AttemptDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
{
var ev = new PsionicHealOtherDoAfterEvent(_gameTiming.CurTime);
ev.HealingAmount = args.HealingAmount;
ev.RotReduction = args.RotReduction;
ev.DoRevive = args.DoRevive;
var doAfterArgs = new DoAfterArgs(EntityManager, uid, args.UseDelay, ev, uid, target: args.Target)
{
BreakOnUserMove = args.BreakOnUserMove,
BreakOnTargetMove = args.BreakOnTargetMove,
};

if (!_doAfterSystem.TryStartDoAfter(doAfterArgs, out var doAfterId))
return;

component.DoAfter = doAfterId;
}

private void OnDispelled(EntityUid uid, PsionicComponent component, DispelledEvent args)
{
if (component.DoAfter is null)
return;

_doAfterSystem.Cancel(component.DoAfter);
component.DoAfter = null;
args.Handled = true;
}

private void OnDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherDoAfterEvent args)
{
// It's entirely possible for the caster to stop being Psionic(due to mindbreaking) mid cast
if (component is null)
return;
component.DoAfter = null;

// The target can also cease existing mid-cast
if (args.Target is null)
return;

_rotting.ReduceAccumulator(args.Target.Value, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification));

if (!TryComp<DamageableComponent>(args.Target.Value, out var damageableComponent))
return;

_damageable.TryChangeDamage(args.Target.Value, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid);

if (!args.DoRevive
|| !TryComp<MobStateComponent>(args.Target, out var mob)
|| !_mobThreshold.TryGetThresholdForState(args.Target.Value, MobState.Dead, out var threshold)
|| damageableComponent.TotalDamage > threshold)
return;

_mobState.ChangeMobState(args.Target.Value, MobState.Critical, mob, uid);
}

// This would be the same thing as OnDoAfter, except that here the target isn't nullable, so I have to reuse code with different arguments.
private void ActivatePower(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
{
if (component is null)
return;

_rotting.ReduceAccumulator(args.Target, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification));

if (!TryComp<DamageableComponent>(args.Target, out var damageableComponent))
return;

_damageable.TryChangeDamage(args.Target, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid);

if (!args.DoRevive
|| !TryComp<MobStateComponent>(args.Target, out var mob)
|| !_mobThreshold.TryGetThresholdForState(args.Target, MobState.Dead, out var threshold)
|| damageableComponent.TotalDamage > threshold)
return;

_mobState.ChangeMobState(args.Target, MobState.Critical, mob, uid);
}
}
59 changes: 59 additions & 0 deletions Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Robust.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Popups;

namespace Content.Shared.Actions.Events;
public sealed partial class PsionicHealOtherPowerActionEvent : EntityTargetActionEvent
{
[DataField]
public DamageSpecifier HealingAmount = default!;

[DataField]
public string PowerName;

/// Controls whether or not a power fires immediately and with no DoAfter
[DataField]
public bool Immediate;

[DataField]
public string? PopupText;

[DataField]
public float RotReduction;

[DataField]
public bool DoRevive;

[DataField]
public bool BreakOnUserMove = true;

[DataField]
public bool BreakOnTargetMove = false;

[DataField]
public float UseDelay = 8f;

[DataField]
public int MinGlimmer = 8;

[DataField]
public int MaxGlimmer = 12;

[DataField]
public int GlimmerObviousSoundThreshold;

[DataField]
public int GlimmerObviousPopupThreshold;

[DataField]
public PopupType PopupType = PopupType.Medium;

[DataField]
public AudioParams AudioParams = default!;

[DataField]
public bool PlaySound;

[DataField]
public SoundSpecifier SoundUse = new SoundPathSpecifier("/Audio/Psionics/heartbeat_fast.ogg");
}
61 changes: 46 additions & 15 deletions Content.Shared/Psionics/Events.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
using Robust.Shared.Serialization;
using Content.Shared.Damage;
using Content.Shared.DoAfter;

namespace Content.Shared.Psionics.Events
namespace Content.Shared.Psionics.Events;

[Serializable, NetSerializable]
public sealed partial class PsionicRegenerationDoAfterEvent : DoAfterEvent
{
[Serializable, NetSerializable]
public sealed partial class PsionicRegenerationDoAfterEvent : DoAfterEvent
[DataField("startedAt", required: true)]
public TimeSpan StartedAt;

public PsionicRegenerationDoAfterEvent(TimeSpan startedAt)
{
[DataField("startedAt", required: true)]
public TimeSpan StartedAt;
StartedAt = startedAt;
}

public override DoAfterEvent Clone() => this;
}

private PsionicRegenerationDoAfterEvent()
{
}
[Serializable, NetSerializable]
public sealed partial class GlimmerWispDrainDoAfterEvent : SimpleDoAfterEvent { }

public PsionicRegenerationDoAfterEvent(TimeSpan startedAt)
{
StartedAt = startedAt;
}
[Serializable, NetSerializable]
public sealed partial class HealingWordDoAfterEvent : DoAfterEvent
{
[DataField(required: true)]
public TimeSpan StartedAt;

public override DoAfterEvent Clone() => this;
public HealingWordDoAfterEvent(TimeSpan startedAt)
{
StartedAt = startedAt;
}

[Serializable, NetSerializable]
public sealed partial class GlimmerWispDrainDoAfterEvent : SimpleDoAfterEvent
public override DoAfterEvent Clone() => this;
}

[Serializable, NetSerializable]
public sealed partial class PsionicHealOtherDoAfterEvent : DoAfterEvent
{
[DataField(required: true)]
public TimeSpan StartedAt;

[DataField]
public DamageSpecifier HealingAmount = default!;

[DataField]
public float RotReduction;

[DataField]
public bool DoRevive;

public PsionicHealOtherDoAfterEvent(TimeSpan startedAt)
{
StartedAt = startedAt;
}

public override DoAfterEvent Clone() => this;
}
11 changes: 10 additions & 1 deletion Content.Shared/Psionics/PsionicComponent.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Content.Shared.DoAfter;
using Content.Shared.Psionics;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
Expand Down Expand Up @@ -166,6 +167,14 @@ private set
/// unneccesary subs for unique psionic entities like e.g. Oracle.
/// </summary>
[DataField]
public List<String>? PsychognomicDescriptors = null;
public List<string>? PsychognomicDescriptors = null;

/// Used for tracking what spell a Psion is actively casting
[DataField]
public DoAfterId? DoAfter;

/// Popup to play if a Psion attempts to start casting a power while already casting one
[DataField]
public string AlreadyCasting = "already-casting";
}
}
23 changes: 23 additions & 0 deletions Resources/Locale/en-US/psionics/psionic-powers.ftl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
generic-power-initialization-feedback = I Awaken.
arleady-casting = I cannot channel more than one power at a time.

# Dispel
dispel-power-description = Dispel summoned entities such as familiars or forcewalls.
Expand Down Expand Up @@ -48,6 +49,28 @@ psionic-regeneration-power-initialization-feedback =
I look within myself, finding a wellspring of life.
psionic-regeneration-power-metapsionic-feedback = {CAPITALIZE($entity)} possesses an overwhelming will to live

# Healing Word
action-name-healing-word = Healing Word
action-description-healing-word = Speak the Lesser Secret Of Life, and restore health to another.
healing-word-power-description = Speak the Lesser Secret Of Life, and restore health to another.
healing-word-power-initialization-feedback =
At the beginning of time, a word was spoken that brought life into the Spheres.
Though it taxes my mind to know it, this Secret is known to me now.
I need only speak it.
healing-word-power-metapsionic-feedback = {CAPITALIZE($entity)} bears the Lesser Secret of Life.
healing-word-begin = {CAPITALIZE($entity)} mutters a word that brings both joy and pain alike to those who hear it.

# Revivify
action-name-revivify = Breath of Life
action-description-revivify = Speak the Greater Secret of Life, and restore another to life.
revivify-power-description = Speak the Greater Secret of Life, and restore another to life.
revivify-power-initialization-feedback =
For a moment, my soul journeys across time and space to the beginning of it all, there I hear it.
The Secret of Life in its fullness. I feel my entire existence burning out from within, merely by knowing it.
Power flows through me as a mighty river, begging to be released with a simple spoken word.
revivify-power-metapsionic-feedback = {CAPITALIZE($entity)} bears the Greater Secret of Life.
revivify-word-begin = {CAPITALIZE($entity)} enunciates a word of such divine power, that those who hear it weep from joy.

# Telegnosis
telegnosis-power-description = Create a telegnostic projection to remotely observe things.
telegnosis-power-initialization-feedback =
Expand Down
Loading
Loading