Skip to content

Commit

Permalink
Feat: Port Delta-V Glimmer Wisps (#1125)
Browse files Browse the repository at this point in the history
# Description
Ports DeltaV-Station/Delta-v#1383 with all due
changes.

Also moves some files from delta-v/nyano namespaces to the root
namespace.

# TODO
- [X] Port fun
- [X] Test fun
- [X] Record fun

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

I had to compress the shit out of this video, so it's about as good as
it can get.


https://github.com/user-attachments/assets/3ab9d328-ae6e-4a6c-8a48-9da22783579a

</p>
</details>

---

# Changelog
:cl:
- add: Raising glimmer too high can now cause glimmer wisps to start
haunting the station.

---------

Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
  • Loading branch information
Mnemotechnician and deltanedas authored Oct 23, 2024
1 parent b73d45e commit dd9cdc1
Show file tree
Hide file tree
Showing 33 changed files with 906 additions and 234 deletions.

This file was deleted.

88 changes: 0 additions & 88 deletions Content.Server/DeltaV/StationEvents/Events/GlimmerMobSpawnRule.cs

This file was deleted.

60 changes: 60 additions & 0 deletions Content.Server/LifeDrainer/LifeDrainerComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;

namespace Content.Server.LifeDrainer;

/// <summary>
/// Adds a verb to drain life from a crit mob that matches a whitelist.
/// Successfully draining a mob rejuvenates you completely.
/// </summary>
[RegisterComponent, Access(typeof(LifeDrainerSystem))]
public sealed partial class LifeDrainerComponent : Component
{
/// <summary>
/// Damage to give to the target when draining is complete
/// </summary>
[DataField(required: true)]
public DamageSpecifier Damage = new();

/// <summary>
/// Mobs have to match this whitelist to be drained.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;

/// <summary>
/// The time that it takes to drain an entity.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(8.35f);

/// <summary>
/// Sound played while draining a mob.
/// </summary>
[DataField]
public SoundSpecifier DrainSound = new SoundPathSpecifier("/Audio/DeltaV/Effects/clang2.ogg");

/// <summary>
/// Sound played after draining is complete.
/// </summary>
[DataField]
public SoundSpecifier FinishSound = new SoundPathSpecifier("/Audio/Effects/guardian_inject.ogg");

[DataField]
public EntityUid? DrainStream;

/// <summary>
/// A current drain doafter in progress.
/// </summary>
[DataField]
public DoAfterId? DoAfter;

/// <summary>
/// What mob is being targeted for draining.
/// When draining stops the AI will try to drain this target again until successful.
/// </summary>
[DataField]
public EntityUid? Target;
}
143 changes: 143 additions & 0 deletions Content.Server/LifeDrainer/LifeDrainerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using Content.Server.Carrying;
using Content.Server.NPC.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Popups;
using Content.Shared.Rejuvenate;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Utility;

namespace Content.Server.LifeDrainer;

public sealed class LifeDrainerSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;

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

SubscribeLocalEvent<LifeDrainerComponent, GetVerbsEvent<InnateVerb>>(OnGetVerbs);
SubscribeLocalEvent<LifeDrainerComponent, LifeDrainDoAfterEvent>(OnDrain);
}

private void OnGetVerbs(Entity<LifeDrainerComponent> ent, ref GetVerbsEvent<InnateVerb> args)
{
var target = args.Target;
if (!args.CanAccess || !args.CanInteract || !CanDrain(ent, target))
return;

args.Verbs.Add(new InnateVerb()
{
Act = () =>
{
TryDrain(ent, target);
},
Text = Loc.GetString("verb-life-drain"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Nyanotrasen/Icons/verbiconfangs.png")),
Priority = 2
});
}

private void OnDrain(Entity<LifeDrainerComponent> ent, ref LifeDrainDoAfterEvent args)
{
var (uid, comp) = ent;
CancelDrain(comp);
if (args.Handled || args.Args.Target is not {} target)
return;

// attack whoever interrupted the draining
if (args.Cancelled)
{
// someone pulled the psionic away
if (TryComp<PullableComponent>(target, out var pullable) && pullable.Puller is {} puller)
_faction.AggroEntity(uid, puller);

// someone pulled me away
if (TryComp(ent, out pullable) && pullable.Puller is {} selfPuller)
_faction.AggroEntity(uid, selfPuller);

// someone carried the psionic away
if (TryComp<BeingCarriedComponent>(target, out var carried))
_faction.AggroEntity(uid, carried.Carrier);

return;
}

_popup.PopupEntity(Loc.GetString("life-drain-second-end", ("drainer", uid)), target, target, PopupType.LargeCaution);
_popup.PopupEntity(Loc.GetString("life-drain-third-end", ("drainer", uid), ("target", target)), target, Filter.PvsExcept(target), true, PopupType.LargeCaution);

var rejuv = new RejuvenateEvent();
RaiseLocalEvent(uid, rejuv);

_audio.PlayPvs(comp.FinishSound, uid);

_damageable.TryChangeDamage(target, comp.Damage, true, origin: uid);
}

public bool CanDrain(Entity<LifeDrainerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
return !IsDraining(comp)
&& uid != target
&& (comp.Whitelist is null || _whitelist.IsValid(comp.Whitelist, target))
&& _mob.IsCritical(target);
}

public bool IsDraining(LifeDrainerComponent comp)
{
return _doAfter.GetStatus(comp.DoAfter) == DoAfterStatus.Running;
}

public bool TryDrain(Entity<LifeDrainerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
if (!CanDrain(ent, target) || !_actionBlocker.CanInteract(uid, target) || !_interaction.InRangeUnobstructed(ent, target, popup: true))
return false;

_popup.PopupEntity(Loc.GetString("life-drain-second-start", ("drainer", uid)), target, target, PopupType.LargeCaution);
_popup.PopupEntity(Loc.GetString("life-drain-third-start", ("drainer", uid), ("target", target)), target, Filter.PvsExcept(target), true, PopupType.LargeCaution);

if (_audio.PlayPvs(comp.DrainSound, target) is {} stream)
comp.DrainStream = stream.Item1;

var ev = new LifeDrainDoAfterEvent();
var args = new DoAfterArgs(EntityManager, uid, comp.Delay, ev, target: target, eventTarget: uid)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
MovementThreshold = 2f,
NeedHand = false
};

if (!_doAfter.TryStartDoAfter(args, out var id))
return false;

comp.DoAfter = id;
comp.Target = target;
return true;
}

public void CancelDrain(LifeDrainerComponent comp)
{
comp.DrainStream = _audio.Stop(comp.DrainStream);
_doAfter.Cancel(comp.DoAfter);
comp.DoAfter = null;
comp.Target = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Content.Server.LifeDrainer;

namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;

public sealed partial class DrainOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;

private LifeDrainerSystem _drainer = default!;
private EntityQuery<LifeDrainerComponent> _drainerQuery;

[DataField(required: true)]
public string DrainKey = string.Empty;

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

_drainer = sysManager.GetEntitySystem<LifeDrainerSystem>();
_drainerQuery = _entMan.GetEntityQuery<LifeDrainerComponent>();
}

public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
var target = blackboard.GetValue<EntityUid>(DrainKey);

if (_entMan.Deleted(target))
return HTNOperatorStatus.Failed;

if (!_drainerQuery.TryComp(owner, out var wisp))
return HTNOperatorStatus.Failed;

// still draining hold your horses
if (_drainer.IsDraining(wisp))
return HTNOperatorStatus.Continuing;

// not draining and no target set, start to drain
if (wisp.Target == null)
{
return _drainer.TryDrain((owner, wisp), target)
? HTNOperatorStatus.Continuing
: HTNOperatorStatus.Failed;
}

// stopped draining, clean up and find another one after
_drainer.CancelDrain(wisp);
return HTNOperatorStatus.Finished;
}
}
Loading

0 comments on commit dd9cdc1

Please sign in to comment.