diff --git a/Content.Client/Revenant/RevenantRegenModifierSystem.cs b/Content.Client/Revenant/RevenantRegenModifierSystem.cs new file mode 100644 index 00000000000000..1fc08ab0ac337c --- /dev/null +++ b/Content.Client/Revenant/RevenantRegenModifierSystem.cs @@ -0,0 +1,56 @@ +using System.Numerics; +using Content.Client.Alerts; +using Content.Shared.Revenant; +using Content.Shared.Revenant.Components; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Client.Revenant; + +public sealed class RevenantRegenModifierSystem : EntitySystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + + private readonly SpriteSpecifier _witnessIndicator = new SpriteSpecifier.Texture(new ResPath("Interface/Actions/scream.png")); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUpdateAlert); + SubscribeNetworkEvent(OnWitnesses); + } + + private void OnWitnesses(RevenantHauntWitnessEvent args) + { + foreach (var witness in args.Witnesses) + { + var ent = GetEntity(witness); + if (TryComp(ent, out var sprite)) + { + var layerID = sprite.AddLayer(_witnessIndicator); + if (sprite.TryGetLayer(layerID, out var layer)) + { + layer.Offset = new Vector2(0, 0.8f); + layer.Scale = new Vector2(0.65f, 0.65f); + } + Timer.Spawn(TimeSpan.FromSeconds(5), () => sprite.RemoveLayer(layerID)); + } + } + } + + private void OnUpdateAlert(Entity ent, ref UpdateAlertSpriteEvent args) + { + if (args.Alert.ID != ent.Comp.Alert) + return; + + var sprite = args.SpriteViewEnt.Comp; + var witnesses = Math.Clamp(ent.Comp.Witnesses.Count, 0, 99); + sprite.LayerSetState(RevenantVisualLayers.Digit1, $"{witnesses / 10}"); + sprite.LayerSetState(RevenantVisualLayers.Digit2, $"{witnesses % 10}"); + } +} \ No newline at end of file diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index 6526ff0fa8bf14..a00ecacab48874 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -32,6 +32,10 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Components; +using Robust.Shared.Player; +using Content.Shared.StatusEffect; +using Content.Shared.Flash.Components; +using Robust.Shared.Audio.Systems; namespace Content.Server.Revenant.EntitySystems; @@ -48,6 +52,13 @@ public sealed partial class RevenantSystem [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly RevenantAnimatedSystem _revenantAnimated = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + + [ValidatePrototypeId] + private const string RevenantEssenceRegen = "EssenceRegen"; + + [ValidatePrototypeId] + private const string FlashedId = "Flashed"; private void InitializeAbilities() { @@ -61,6 +72,7 @@ private void InitializeAbilities() SubscribeLocalEvent(OnMalfunctionAction); SubscribeLocalEvent(OnBloodWritingAction); SubscribeLocalEvent(OnAnimateAction); + SubscribeLocalEvent(OnHauntAction); } private void OnInteract(EntityUid uid, RevenantComponent component, UserActivateInWorldEvent args) @@ -220,6 +232,59 @@ private void OnHarvest(EntityUid uid, RevenantComponent component, HarvestEvent args.Handled = true; } + private void OnHauntAction(EntityUid uid, RevenantComponent comp, RevenantHauntActionEvent args) + { + if (args.Handled) + return; + + if (!TryUseAbility(uid, comp, 0, comp.HauntDebuffs)) + return; + + args.Handled = true; + + // This is probably not the right way to do this... + var witnessAndRevenantFilter = Filter.Pvs(uid).RemoveWhere(player => + { + if (player.AttachedEntity == null) + return true; + + var ent = player.AttachedEntity.Value; + + if (!HasComp(ent) || !HasComp(ent) || HasComp(ent)) + return true; + + return !_interact.InRangeUnobstructed((uid, Transform(uid)), (ent, Transform(ent)), range: 0, collisionMask: CollisionGroup.Impassable); + }); + + var witnesses = new HashSet(witnessAndRevenantFilter.RemovePlayerByAttachedEntity(uid).Recipients.Select(ply => GetNetEntity(ply.AttachedEntity!.Value))); + + // Give the witnesses a spook! + _audioSystem.PlayGlobal(comp.HauntSound, witnessAndRevenantFilter, true); + + foreach (var witness in witnesses) + { + _statusEffects.TryAddStatusEffect(GetEntity(witness), + FlashedId, + comp.HauntFlashDuration, + false + ); + } + + if (witnesses.Count > 0 && _statusEffects.TryAddStatusEffect(uid, + RevenantEssenceRegen, + comp.HauntEssenceRegenDuration, + true, + component: new RevenantRegenModifierComponent(witnesses) + )) + { + if (_mind.TryGetMind(uid, out var _, out var mind) && mind.Session != null) + RaiseNetworkEvent(new RevenantHauntWitnessEvent(witnesses), mind.Session); + + _store.TryAddCurrency(new Dictionary + { {comp.StolenEssenceCurrencyPrototype, comp.HauntStolenEssencePerWitness * witnesses.Count} }, uid); + } + } + private void OnDefileAction(EntityUid uid, RevenantComponent component, RevenantDefileActionEvent args) { if (args.Handled) diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index 496fe4d7ce5d60..9ac70ceadfc27c 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -55,6 +55,9 @@ public sealed partial class RevenantSystem : EntitySystem [ValidatePrototypeId] private const string RevenantShopId = "ActionRevenantShop"; + [ValidatePrototypeId] + private const string RevenantHauntId = "ActionRevenantHaunt"; + public override void Initialize() { base.Initialize(); @@ -98,7 +101,8 @@ private void OnStartup(EntityUid uid, RevenantComponent component, ComponentStar private void OnMapInit(EntityUid uid, RevenantComponent component, MapInitEvent args) { - _action.AddAction(uid, ref component.Action, RevenantShopId); + _action.AddAction(uid, ref component.ShopAction, RevenantShopId); + _action.AddAction(uid, ref component.HauntAction, RevenantHauntId); } private void OnStatusAdded(EntityUid uid, RevenantComponent component, StatusEffectAddedEvent args) @@ -232,7 +236,12 @@ public override void Update(float frameTime) if (rev.Essence < rev.EssenceRegenCap) { - ChangeEssenceAmount(uid, rev.EssencePerSecond, rev, regenCap: true); + var essence = rev.EssencePerSecond; + + if (TryComp(uid, out var regen)) + essence += rev.HauntEssenceRegenPerWitness * regen.Witnesses.Count; + + ChangeEssenceAmount(uid, essence, rev, regenCap: true); } } } diff --git a/Content.Shared/Revenant/Components/RevenantComponent.cs b/Content.Shared/Revenant/Components/RevenantComponent.cs index 8066291729e3d0..d2308a4cdbceaf 100644 --- a/Content.Shared/Revenant/Components/RevenantComponent.cs +++ b/Content.Shared/Revenant/Components/RevenantComponent.cs @@ -3,6 +3,7 @@ using Content.Shared.FixedPoint; using Content.Shared.Store; using Content.Shared.Whitelist; +using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -92,6 +93,30 @@ public sealed partial class RevenantComponent : Component public float MaxEssenceUpgradeAmount = 10; #endregion + // When used, the revenant reveals itself temporarily and gains stolen essence and a boost in + // essence regeneration for each crewmate that witnesses it + #region Haunt Ability + + [DataField("hauntDebuffs"), ViewVariables(VVAccess.ReadWrite)] + public Vector2 HauntDebuffs = new(2, 6); + + [DataField("hauntStolenEssencePerWitness"), ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 HauntStolenEssencePerWitness = 2.5; + + [DataField("hauntEssenceRegenPerWitness"), ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 HauntEssenceRegenPerWitness = 0.5; + + [DataField("hauntEssenceRegenDuration"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan HauntEssenceRegenDuration = TimeSpan.FromSeconds(10); + + [DataField("hauntSound"), ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier? HauntSound = new SoundCollectionSpecifier("RevenantHaunt"); + + [DataField("hauntFlashDuration"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan HauntFlashDuration = TimeSpan.FromSeconds(2); + + #endregion + //In the nearby radius, causes various objects to be thrown, messed with, and containers opened //Generally just causes a mess #region Defile Ability @@ -260,5 +285,6 @@ public sealed partial class RevenantComponent : Component public string HarvestingState = "harvesting"; #endregion - [DataField] public EntityUid? Action; + [DataField] public EntityUid? ShopAction; + [DataField] public EntityUid? HauntAction; } diff --git a/Content.Shared/Revenant/Components/RevenantRegenModifierComponent.cs b/Content.Shared/Revenant/Components/RevenantRegenModifierComponent.cs new file mode 100644 index 00000000000000..0bc8b436715a92 --- /dev/null +++ b/Content.Shared/Revenant/Components/RevenantRegenModifierComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Alert; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Revenant.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class RevenantRegenModifierComponent : Component +{ + [ViewVariables, AutoNetworkedField] + public HashSet Witnesses; + + [DataField] + public ProtoId Alert = "EssenceRegen"; + + public RevenantRegenModifierComponent(HashSet witnesses) + { + Witnesses = witnesses; + } + + public RevenantRegenModifierComponent() : this(new()) + { + } +} \ No newline at end of file diff --git a/Content.Shared/Revenant/SharedRevenant.cs b/Content.Shared/Revenant/SharedRevenant.cs index cfd7c779113026..b073dcc8d598fd 100644 --- a/Content.Shared/Revenant/SharedRevenant.cs +++ b/Content.Shared/Revenant/SharedRevenant.cs @@ -46,6 +46,10 @@ public sealed partial class RevenantShopActionEvent : InstantActionEvent { } +public sealed partial class RevenantHauntActionEvent : InstantActionEvent +{ +} + public sealed partial class RevenantDefileActionEvent : InstantActionEvent { } @@ -70,6 +74,21 @@ public sealed partial class RevenantAnimateEvent : EntityTargetActionEvent { } +[Serializable, NetSerializable] +public sealed partial class RevenantHauntWitnessEvent : EntityEventArgs +{ + public HashSet Witnesses = new(); + + public RevenantHauntWitnessEvent(HashSet witnesses) + { + Witnesses = witnesses; + } + + public RevenantHauntWitnessEvent() : this(new()) + { + } +} + [Serializable, NetSerializable] public sealed partial class ExorciseRevenantDoAfterEvent : SimpleDoAfterEvent { diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index 000d3f3cc396d9..b5764e6e9acfd2 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -144,6 +144,22 @@ public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool re return false; } + public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, Component component, + StatusEffectsComponent? status = null) + { + if (!Resolve(uid, ref status, false)) + return false; + + if (TryAddStatusEffect(uid, key, time, refresh, status)) + { + EntityManager.AddComponent(uid, component, true); + status.ActiveEffects[key].RelevantComponent = _componentFactory.GetComponentName(component.GetType()); + return true; + } + + return false; + } + /// /// Tries to add a status effect to an entity with a certain timer. /// diff --git a/Resources/Audio/Effects/Revenant/attributions.yml b/Resources/Audio/Effects/Revenant/attributions.yml new file mode 100644 index 00000000000000..5fc32304e42ecd --- /dev/null +++ b/Resources/Audio/Effects/Revenant/attributions.yml @@ -0,0 +1,24 @@ +- files: ["haunt0.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Sound originally derived from SCP: Containment breach." + source: "https://github.com/Regalis11/scpcb/blob/edb8fe0840b78f14d1aef3a0bf6174630e7be296/SFX/Horror/Horror0.ogg" + +- files: ["haunt1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Sound originally derived from SCP: Containment breach." + source: "https://github.com/Regalis11/scpcb/blob/edb8fe0840b78f14d1aef3a0bf6174630e7be296/SFX/Horror/Horror4.ogg" + +- files: ["haunt2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Sound originally derived from SCP: Containment breach." + source: "https://github.com/Regalis11/scpcb/blob/edb8fe0840b78f14d1aef3a0bf6174630e7be296/SFX/Horror/Horror6.ogg" + +- files: ["haunt3.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Sound originally derived from SCP: Containment breach." + source: "https://github.com/Regalis11/scpcb/blob/edb8fe0840b78f14d1aef3a0bf6174630e7be296/SFX/Horror/Horror11.ogg" + +- files: ["haunt4.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Sound originally derived from SCP: Containment breach. Edited down to a shorter length by TGRCDev." + source: "https://github.com/Regalis11/scpcb/blob/edb8fe0840b78f14d1aef3a0bf6174630e7be296/SFX/Horror/Horror12.ogg" diff --git a/Resources/Audio/Effects/Revenant/haunt0.ogg b/Resources/Audio/Effects/Revenant/haunt0.ogg new file mode 100644 index 00000000000000..13d49ad0860543 Binary files /dev/null and b/Resources/Audio/Effects/Revenant/haunt0.ogg differ diff --git a/Resources/Audio/Effects/Revenant/haunt1.ogg b/Resources/Audio/Effects/Revenant/haunt1.ogg new file mode 100644 index 00000000000000..fe22b62659418c Binary files /dev/null and b/Resources/Audio/Effects/Revenant/haunt1.ogg differ diff --git a/Resources/Audio/Effects/Revenant/haunt2.ogg b/Resources/Audio/Effects/Revenant/haunt2.ogg new file mode 100644 index 00000000000000..4fbd42d68769ed Binary files /dev/null and b/Resources/Audio/Effects/Revenant/haunt2.ogg differ diff --git a/Resources/Audio/Effects/Revenant/haunt3.ogg b/Resources/Audio/Effects/Revenant/haunt3.ogg new file mode 100644 index 00000000000000..2d182516abe38c Binary files /dev/null and b/Resources/Audio/Effects/Revenant/haunt3.ogg differ diff --git a/Resources/Audio/Effects/Revenant/haunt4.ogg b/Resources/Audio/Effects/Revenant/haunt4.ogg new file mode 100644 index 00000000000000..94859eb2a07efe Binary files /dev/null and b/Resources/Audio/Effects/Revenant/haunt4.ogg differ diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 1c313641f37dff..9e66e1f3a0e2d3 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -111,6 +111,9 @@ alerts-revenant-essence-desc = The power of souls. It sustains you and is used f alerts-revenant-corporeal-name = Corporeal alerts-revenant-corporeal-desc = You have manifested physically. People around you can see and hurt you. +alerts-revenant-essence-regen-name = Haunting +alerts-revenant-essence-regen-desc = You are haunting the crew! You have a boosted essence regeneration rate based on how many people you haunted. + alerts-changeling-chemicals-name = Chemicals alerts-changeling-chemicals-desc = Spend chemicals to use your abilities. Slowly regenerates. diff --git a/Resources/Prototypes/Actions/revenant.yml b/Resources/Prototypes/Actions/revenant.yml index c36a530f688d71..35ced8756f6f02 100644 --- a/Resources/Prototypes/Actions/revenant.yml +++ b/Resources/Prototypes/Actions/revenant.yml @@ -7,6 +7,19 @@ icon: Interface/Actions/shop.png event: !type:RevenantShopActionEvent +- type: entity + id: ActionRevenantHaunt + name: Haunt + description: Gives essence and stolen essence for every witness. + components: + - type: InstantAction + itemIconStyle: NoItem + icon: + sprite: Mobs/Ghosts/revenant.rsi + state: icon + event: !type:RevenantHauntActionEvent + useDelay: 15 + - type: entity id: ActionRevenantDefile name: Defile diff --git a/Resources/Prototypes/Alerts/revenant.yml b/Resources/Prototypes/Alerts/revenant.yml index fdd4e6c87ca763..00d2e869ec0ced 100644 --- a/Resources/Prototypes/Alerts/revenant.yml +++ b/Resources/Prototypes/Alerts/revenant.yml @@ -14,6 +14,13 @@ name: alerts-revenant-corporeal-name description: alerts-revenant-corporeal-desc +- type: alert + id: EssenceRegen + name: alerts-revenant-essence-regen-name + description: alerts-revenant-essence-regen-desc + icons: [ /Textures/Interface/Actions/scream.png ] + alertViewEntity: AlertEssenceRegenSpriteView + - type: alert id: Stasis icons: @@ -35,3 +42,14 @@ offset: 0.125, 0 - map: [ "enum.RevenantVisualLayers.Digit3" ] offset: 0.25, 0 + +- type: entity + id: AlertEssenceRegenSpriteView + components: + - type: Sprite + sprite: /Textures/Interface/Alerts/essence_counter.rsi + layers: + - map: [ "enum.AlertVisualLayers.Base" ] + - map: [ "enum.RevenantVisualLayers.Digit1" ] + - map: [ "enum.RevenantVisualLayers.Digit2" ] + offset: 0.125, 0 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index e220bf73b52f73..3fc86a3fac4399 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -19,6 +19,7 @@ allowed: - Stun - Corporeal + - EssenceRegen - type: Hands showInHands: false disableExplosionRecursion: true diff --git a/Resources/Prototypes/SoundCollections/revenant.yml b/Resources/Prototypes/SoundCollections/revenant.yml new file mode 100644 index 00000000000000..53d0f9c99d4139 --- /dev/null +++ b/Resources/Prototypes/SoundCollections/revenant.yml @@ -0,0 +1,8 @@ +- type: soundCollection + id: RevenantHaunt + files: + - /Audio/Effects/Revenant/haunt0.ogg + - /Audio/Effects/Revenant/haunt1.ogg + - /Audio/Effects/Revenant/haunt2.ogg + - /Audio/Effects/Revenant/haunt3.ogg + - /Audio/Effects/Revenant/haunt4.ogg \ No newline at end of file diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index 2f3ba292296cdc..e269851375c690 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -50,6 +50,10 @@ alert: Stasis alwaysAllowed: true +- type: statusEffect + id: EssenceRegen + alert: EssenceRegen + - type: statusEffect id: ForcedSleep #I.e., they will not wake on damage or similar