Skip to content

Commit

Permalink
Psionic "Heal Other" Powers (#942)
Browse files Browse the repository at this point in the history
# Description

This PR introduces two new Psionic Powers, Healing Word, and Breath of
Life, both utilizing a new PsionicHealOtherSystem, which operates on
datafield event arguments rather than a "hardcoded" component. Thus, any
number of powers can be created which share this system.

Healing Word is a power that features a short cast time, and heals a
small amount of each damage type to a target(while reducing the target's
rot timer slightly). It has a relatively short cooldown, and a low
glimmer cost.

Breath of Life by contrast, is an extremely rare power with a longer
cast time, healing a much larger amount of each damage type to a target,
reduces rot significantly, and attempts to revive the target. It has a 2
minute cooldown, and a high glimmer cost.

<details><summary><h1>Media</h1></summary>
<p>


![image](https://github.com/user-attachments/assets/ba01ccce-639f-4b03-84bb-55f96b5aeda3)

</p>
</details>

# Changelog

:cl:
- add: Healing Word has been added as a new Psionic Power. When cast on
another person, it heals a small amount of every damage type(scaling
with Casting Stats), while also reducing rot timers. Healing Word has a
very short cooldown, and a fairly low Glimmer cost.
- add: Breath of Life has been added as a new extremely rare Psionic
Power. When cast on another person, it heals a large amount of
damage(scaling with Casting Stats), while also substantially reducing
rot timers. Additionally, it will revive the target if it is possible to
do so. Breath of Life has an incredibly long cooldown, a long
interuptable cast time, and an extraordinarily high glimmer cost(A
typical Psion will spike glimmer by more than 50 points when casting
it).
- add: The Chaplain now starts with the Healing Word power.

---------

Signed-off-by: VMSolidus <[email protected]>
Co-authored-by: DEATHB4DEFEAT <[email protected]>
  • Loading branch information
VMSolidus and DEATHB4DEFEAT committed Sep 21, 2024
1 parent 1b43123 commit 61e1c8c
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 20 deletions.
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!;


public override void Initialize()
{
base.Initialize();

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


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

0 comments on commit 61e1c8c

Please sign in to comment.