-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# 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
1 parent
1b43123
commit 61e1c8c
Showing
13 changed files
with
412 additions
and
20 deletions.
There are no files selected for viewing
152 changes: 152 additions & 0 deletions
152
Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
59
Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.