Revs (the spooky ones) (#9842)
This commit is contained in:
15
Content.Server/Revenant/CorporealComponent.cs
Normal file
15
Content.Server/Revenant/CorporealComponent.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Content.Server.Revenant;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the target solid, visible, and applies a slowdown.
|
||||
/// Meant to be used in conjunction with statusEffectSystem
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class CorporealComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The debuff applied when the component is present.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MovementSpeedDebuff = 0.66f;
|
||||
}
|
||||
79
Content.Server/Revenant/EntitySystems/CorporealSystem.cs
Normal file
79
Content.Server/Revenant/EntitySystems/CorporealSystem.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Content.Server.Visible;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Revenant;
|
||||
using Content.Shared.Movement;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.Physics;
|
||||
using System.Linq;
|
||||
using Content.Shared.Movement.Systems;
|
||||
|
||||
namespace Content.Server.Revenant.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the revenant solid when the component is applied.
|
||||
/// Additionally applies a few visual effects.
|
||||
/// Used for status effect.
|
||||
/// </summary>
|
||||
public sealed class CorporealSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CorporealComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<CorporealComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<CorporealComponent, RefreshMovementSpeedModifiersEvent>(OnRefresh);
|
||||
}
|
||||
|
||||
private void OnRefresh(EntityUid uid, CorporealComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
args.ModifySpeed(component.MovementSpeedDebuff, component.MovementSpeedDebuff);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, CorporealComponent component, ComponentStartup args)
|
||||
{
|
||||
if (TryComp<AppearanceComponent>(uid, out var app))
|
||||
app.SetData(RevenantVisuals.Corporeal, true);
|
||||
|
||||
if (TryComp<FixturesComponent>(uid, out var fixtures) && fixtures.FixtureCount >= 1)
|
||||
{
|
||||
var fixture = fixtures.Fixtures.Values.First();
|
||||
|
||||
fixture.CollisionMask = (int) (CollisionGroup.SmallMobMask | CollisionGroup.GhostImpassable);
|
||||
fixture.CollisionLayer = (int) CollisionGroup.SmallMobLayer;
|
||||
}
|
||||
|
||||
if (TryComp<VisibilityComponent>(uid, out var visibility))
|
||||
{
|
||||
_visibilitySystem.RemoveLayer(visibility, (int) VisibilityFlags.Ghost, false);
|
||||
_visibilitySystem.AddLayer(visibility, (int) VisibilityFlags.Normal, false);
|
||||
_visibilitySystem.RefreshVisibility(visibility);
|
||||
}
|
||||
_movement.RefreshMovementSpeedModifiers(uid);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, CorporealComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (TryComp<AppearanceComponent>(uid, out var app))
|
||||
app.SetData(RevenantVisuals.Corporeal, false);
|
||||
|
||||
if (TryComp<FixturesComponent>(uid, out var fixtures) && fixtures.FixtureCount >= 1)
|
||||
{
|
||||
var fixture = fixtures.Fixtures.Values.First();
|
||||
|
||||
fixture.CollisionMask = (int) CollisionGroup.GhostImpassable;
|
||||
fixture.CollisionLayer = 0;
|
||||
}
|
||||
|
||||
if (TryComp<VisibilityComponent>(uid, out var visibility))
|
||||
{
|
||||
_visibilitySystem.AddLayer(visibility, (int) VisibilityFlags.Ghost, false);
|
||||
_visibilitySystem.RemoveLayer(visibility, (int) VisibilityFlags.Normal, false);
|
||||
_visibilitySystem.RefreshVisibility(visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Content.Server/Revenant/EntitySystems/EssenceSystem.cs
Normal file
71
Content.Server/Revenant/EntitySystems/EssenceSystem.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Revenant.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Attached to entities when a revenant drains them in order to
|
||||
/// manage their essence.
|
||||
/// </summary>
|
||||
public sealed class EssenceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EssenceComponent, ComponentStartup>(UpdateEssenceAmount);
|
||||
SubscribeLocalEvent<EssenceComponent, MobStateChangedEvent>(UpdateEssenceAmount);
|
||||
SubscribeLocalEvent<EssenceComponent, MindAddedMessage>(UpdateEssenceAmount);
|
||||
SubscribeLocalEvent<EssenceComponent, MindRemovedMessage>(UpdateEssenceAmount);
|
||||
SubscribeLocalEvent<EssenceComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, EssenceComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!component.SearchComplete || !HasComp<RevenantComponent>(args.Examiner))
|
||||
return;
|
||||
|
||||
string message;
|
||||
switch (component.EssenceAmount)
|
||||
{
|
||||
case <= 45:
|
||||
message = "revenant-soul-yield-low";
|
||||
break;
|
||||
case >= 90:
|
||||
message = "revenant-soul-yield-high";
|
||||
break;
|
||||
default:
|
||||
message = "revenant-soul-yield-average";
|
||||
break;
|
||||
}
|
||||
|
||||
args.PushMarkup(Loc.GetString(message, ("target", uid)));
|
||||
}
|
||||
|
||||
private void UpdateEssenceAmount(EntityUid uid, EssenceComponent component, EntityEventArgs args)
|
||||
{
|
||||
if (!TryComp<MobStateComponent>(uid, out var mob))
|
||||
return;
|
||||
|
||||
switch (mob.CurrentState)
|
||||
{
|
||||
case DamageState.Alive:
|
||||
if (TryComp<MindComponent>(uid, out var mind) && mind.Mind != null)
|
||||
component.EssenceAmount = _random.NextFloat(75f, 100f);
|
||||
else
|
||||
component.EssenceAmount = _random.NextFloat(45f, 70f);
|
||||
break;
|
||||
case DamageState.Critical:
|
||||
component.EssenceAmount = _random.NextFloat(35f, 50f);
|
||||
break;
|
||||
case DamageState.Dead:
|
||||
component.EssenceAmount = _random.NextFloat(15f, 20f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Revenant;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Ghost;
|
||||
using Robust.Shared.Physics;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Server.Disease;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using System.Linq;
|
||||
using Content.Server.Emag;
|
||||
using Content.Shared.CharacterAppearance.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Revenant.EntitySystems;
|
||||
|
||||
public sealed partial class RevenantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
||||
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
|
||||
[Dependency] private readonly DiseaseSystem _disease = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
|
||||
private void InitializeAbilities()
|
||||
{
|
||||
SubscribeLocalEvent<RevenantComponent, InteractNoHandEvent>(OnInteract);
|
||||
SubscribeLocalEvent<RevenantComponent, SoulSearchDoAfterComplete>(OnSoulSearchComplete);
|
||||
SubscribeLocalEvent<RevenantComponent, HarvestDoAfterComplete>(OnHarvestComplete);
|
||||
SubscribeLocalEvent<RevenantComponent, HarvestDoAfterCancelled>(OnHarvestCancelled);
|
||||
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantDefileActionEvent>(OnDefileAction);
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantOverloadLightsActionEvent>(OnOverloadLightsAction);
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantBlightActionEvent>(OnBlightAction);
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantMalfunctionActionEvent>(OnMalfunctionAction);
|
||||
}
|
||||
|
||||
private void OnInteract(EntityUid uid, RevenantComponent component, InteractNoHandEvent args)
|
||||
{
|
||||
var target = args.Target;
|
||||
if (target == args.User)
|
||||
return;
|
||||
|
||||
if (HasComp<PoweredLightComponent>(target))
|
||||
{
|
||||
args.Handled = _ghost.DoGhostBooEvent(target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidAppearanceComponent>(target) || HasComp<RevenantComponent>(target))
|
||||
return;
|
||||
|
||||
if (!_interact.InRangeUnobstructed(uid, target))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
if (!TryComp<EssenceComponent>(target, out var essence) || !essence.SearchComplete)
|
||||
{
|
||||
EnsureComp<EssenceComponent>(target);
|
||||
BeginSoulSearchDoAfter(uid, target, component);
|
||||
}
|
||||
else
|
||||
{
|
||||
BeginHarvestDoAfter(uid, target, component, essence);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginSoulSearchDoAfter(EntityUid uid, EntityUid target, RevenantComponent revenant)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-soul-searching", ("target", target)), uid, Filter.Entities(uid), PopupType.Medium);
|
||||
revenant.SoulSearchCancelToken = new();
|
||||
var searchDoAfter = new DoAfterEventArgs(uid, revenant.SoulSearchDuration, revenant.SoulSearchCancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
DistanceThreshold = 2,
|
||||
UserFinishedEvent = new SoulSearchDoAfterComplete(target),
|
||||
};
|
||||
_doAfter.DoAfter(searchDoAfter);
|
||||
}
|
||||
|
||||
private void OnSoulSearchComplete(EntityUid uid, RevenantComponent component, SoulSearchDoAfterComplete args)
|
||||
{
|
||||
if (!TryComp<EssenceComponent>(args.Target, out var essence))
|
||||
return;
|
||||
essence.SearchComplete = true;
|
||||
|
||||
string message;
|
||||
switch (essence.EssenceAmount)
|
||||
{
|
||||
case <= 45:
|
||||
message = "revenant-soul-yield-low";
|
||||
break;
|
||||
case >= 90:
|
||||
message = "revenant-soul-yield-high";
|
||||
break;
|
||||
default:
|
||||
message = "revenant-soul-yield-average";
|
||||
break;
|
||||
}
|
||||
_popup.PopupEntity(Loc.GetString(message, ("target", args.Target)), args.Target, Filter.Entities(uid), PopupType.Medium);
|
||||
}
|
||||
|
||||
private void BeginHarvestDoAfter(EntityUid uid, EntityUid target, RevenantComponent revenant, EssenceComponent essence)
|
||||
{
|
||||
if (essence.Harvested)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-soul-harvested"), target, Filter.Entities(uid), PopupType.SmallCaution);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<MobStateComponent>(target, out var mobstate) && mobstate.CurrentState == DamageState.Alive && !HasComp<SleepingComponent>(target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-soul-too-powerful"), target, Filter.Entities(uid));
|
||||
return;
|
||||
}
|
||||
|
||||
revenant.HarvestCancelToken = new();
|
||||
var doAfter = new DoAfterEventArgs(uid, revenant.HarvestDebuffs.X, revenant.HarvestCancelToken.Token, target)
|
||||
{
|
||||
DistanceThreshold = 2,
|
||||
BreakOnUserMove = true,
|
||||
NeedHand = false,
|
||||
UserFinishedEvent = new HarvestDoAfterComplete(target),
|
||||
UserCancelledEvent = new HarvestDoAfterCancelled(),
|
||||
};
|
||||
|
||||
_appearance.SetData(uid, RevenantVisuals.Harvesting, true);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("revenant-soul-begin-harvest", ("target", target)),
|
||||
target, Filter.Pvs(target), PopupType.Large);
|
||||
|
||||
TryUseAbility(uid, revenant, 0, revenant.HarvestDebuffs);
|
||||
_doAfter.DoAfter(doAfter);
|
||||
}
|
||||
|
||||
private void OnHarvestComplete(EntityUid uid, RevenantComponent component, HarvestDoAfterComplete args)
|
||||
{
|
||||
_appearance.SetData(uid, RevenantVisuals.Harvesting, false);
|
||||
|
||||
if (!TryComp<EssenceComponent>(args.Target, out var essence))
|
||||
return;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("revenant-soul-finish-harvest", ("target", args.Target)),
|
||||
args.Target, Filter.Pvs(args.Target), PopupType.LargeCaution);
|
||||
|
||||
essence.Harvested = true;
|
||||
ChangeEssenceAmount(uid, essence.EssenceAmount, component);
|
||||
component.StolenEssence += essence.EssenceAmount;
|
||||
|
||||
if (!TryComp<MobStateComponent>(args.Target, out var mobstate))
|
||||
return;
|
||||
|
||||
if (_mobState.IsAlive(args.Target) || _mobState.IsCritical(args.Target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-max-essence-increased"), uid, Filter.Entities(uid));
|
||||
component.EssenceRegenCap += component.MaxEssenceUpgradeAmount;
|
||||
}
|
||||
|
||||
//KILL THEMMMM
|
||||
var damage = _mobState.GetEarliestDeadState(mobstate, 0)?.threshold;
|
||||
if (damage == null)
|
||||
return;
|
||||
DamageSpecifier dspec = new();
|
||||
dspec.DamageDict.Add("Cellular", damage.Value);
|
||||
_damage.TryChangeDamage(args.Target, dspec, true);
|
||||
}
|
||||
|
||||
private void OnHarvestCancelled(EntityUid uid, RevenantComponent component, HarvestDoAfterCancelled args)
|
||||
{
|
||||
_appearance.SetData(uid, RevenantVisuals.Harvesting, false);
|
||||
}
|
||||
|
||||
private void OnDefileAction(EntityUid uid, RevenantComponent component, RevenantDefileActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryUseAbility(uid, component, component.DefileCost, component.DefileDebuffs))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
//var coords = Transform(uid).Coordinates;
|
||||
//var gridId = coords.GetGridUid(EntityManager);
|
||||
var xform = Transform(uid);
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var map))
|
||||
return;
|
||||
var tiles = map.GetTilesIntersecting(Box2.CenteredAround(xform.WorldPosition,
|
||||
(component.DefileRadius*2, component.DefileRadius))).ToArray();
|
||||
|
||||
_random.Shuffle(tiles);
|
||||
|
||||
for (var i = 0; i < component.DefileTilePryAmount; i++)
|
||||
{
|
||||
if (!tiles.TryGetValue(i, out var value))
|
||||
continue;
|
||||
value.PryTile();
|
||||
}
|
||||
|
||||
var lookup = _lookup.GetEntitiesInRange(uid, component.DefileRadius, LookupFlags.Approximate | LookupFlags.Anchored);
|
||||
var tags = GetEntityQuery<TagComponent>();
|
||||
var entityStorage = GetEntityQuery<EntityStorageComponent>();
|
||||
var items = GetEntityQuery<ItemComponent>();
|
||||
var lights = GetEntityQuery<PoweredLightComponent>();
|
||||
|
||||
foreach (var ent in lookup)
|
||||
{
|
||||
//break windows
|
||||
if (tags.HasComponent(ent) && _tag.HasAnyTag(ent, "Window"))
|
||||
{
|
||||
//hardcoded damage specifiers til i die.
|
||||
var dspec = new DamageSpecifier();
|
||||
dspec.DamageDict.Add("Structural", 15);
|
||||
_damage.TryChangeDamage(ent, dspec);
|
||||
}
|
||||
|
||||
if (!_random.Prob(component.DefileEffectChance))
|
||||
continue;
|
||||
|
||||
//randomly opens some lockers and such.
|
||||
if (entityStorage.TryGetComponent(ent, out var entstorecomp))
|
||||
_entityStorage.OpenStorage(ent, entstorecomp);
|
||||
|
||||
//chucks shit
|
||||
if (items.HasComponent(ent) &&
|
||||
TryComp<PhysicsComponent>(ent, out var phys) && phys.BodyType != BodyType.Static)
|
||||
_throwing.TryThrow(ent, _random.NextAngle().ToWorldVec());
|
||||
|
||||
//flicker lights
|
||||
if (lights.HasComponent(ent))
|
||||
_ghost.DoGhostBooEvent(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOverloadLightsAction(EntityUid uid, RevenantComponent component, RevenantOverloadLightsActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryUseAbility(uid, component, component.OverloadCost, component.OverloadDebuffs))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var poweredLights = GetEntityQuery<PoweredLightComponent>();
|
||||
var lookup = _lookup.GetEntitiesInRange(uid, component.OverloadRadius);
|
||||
|
||||
foreach (var ent in lookup)
|
||||
{
|
||||
if (!poweredLights.HasComponent(ent))
|
||||
continue;
|
||||
|
||||
var ev = new GhostBooEvent(); //light go flicker
|
||||
RaiseLocalEvent(ent, ev);
|
||||
|
||||
if (_random.Prob(component.OverloadBreakChance))
|
||||
{
|
||||
//values
|
||||
_explosion.QueueExplosion(ent, "RevenantElectric", 15, 3, 5, canCreateVacuum: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBlightAction(EntityUid uid, RevenantComponent component, RevenantBlightActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryUseAbility(uid, component, component.BlightCost, component.BlightDebuffs))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var emo = GetEntityQuery<DiseaseCarrierComponent>();
|
||||
|
||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.BlightRadius))
|
||||
if (emo.TryGetComponent(ent, out var comp))
|
||||
_disease.TryInfect(comp, component.BlightDiseasePrototypeId);
|
||||
}
|
||||
|
||||
private void OnMalfunctionAction(EntityUid uid, RevenantComponent component, RevenantMalfunctionActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryUseAbility(uid, component, component.MalfunctionCost, component.MalfunctionDebuffs))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MalfunctionRadius))
|
||||
_emag.DoEmag(ent, ent); //it emags itself. spooky.
|
||||
}
|
||||
}
|
||||
69
Content.Server/Revenant/EntitySystems/RevenantSystem.Shop.cs
Normal file
69
Content.Server/Revenant/EntitySystems/RevenantSystem.Shop.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Revenant;
|
||||
using Content.Server.UserInterface;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
|
||||
namespace Content.Server.Revenant.EntitySystems;
|
||||
|
||||
// TODO: Delete and replace all of this with StoreSystem once that's merged
|
||||
// i'm sorry, but i'm not ultra-optimizing something that's getting deleted in a week.
|
||||
// 8/7/22 -emo (bully me if this exists in the future)
|
||||
public sealed partial class RevenantSystem : EntitySystem
|
||||
{
|
||||
private void InitializeShop()
|
||||
{
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantShopActionEvent>(OnShop);
|
||||
SubscribeLocalEvent<RevenantComponent, RevenantBuyListingMessage>(OnBuy);
|
||||
}
|
||||
|
||||
private void OnShop(EntityUid uid, RevenantComponent component, RevenantShopActionEvent args)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
ToggleUi(component, actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void OnBuy(EntityUid uid, RevenantComponent component, RevenantBuyListingMessage ev)
|
||||
{
|
||||
RevenantStoreListingPrototype? targetListing = null;
|
||||
foreach (var listing in component.Listings)
|
||||
{
|
||||
if (listing.Key.ID == ev.Listing.ID)
|
||||
targetListing = listing.Key;
|
||||
}
|
||||
|
||||
if (targetListing == null)
|
||||
return;
|
||||
component.Listings[targetListing] = false;
|
||||
|
||||
if (component.StolenEssence < ev.Listing.Price)
|
||||
return;
|
||||
component.StolenEssence -= ev.Listing.Price;
|
||||
|
||||
if (_proto.TryIndex<InstantActionPrototype>(ev.Listing.ActionId, out var action))
|
||||
_action.AddAction(uid, new InstantAction(action), null);
|
||||
|
||||
UpdateUserInterface(component);
|
||||
}
|
||||
|
||||
public void ToggleUi(RevenantComponent component, IPlayerSession session)
|
||||
{
|
||||
var ui = component.Owner.GetUIOrNull(RevenantUiKey.Key);
|
||||
ui?.Toggle(session);
|
||||
|
||||
UpdateUserInterface(component);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface(RevenantComponent component)
|
||||
{
|
||||
var ui = component.Owner.GetUIOrNull(RevenantUiKey.Key);
|
||||
if (ui == null)
|
||||
return;
|
||||
|
||||
var filterlistings = (from e in component.Listings where e.Value select e.Key).ToList();
|
||||
|
||||
ui.SetState(new RevenantUpdateState(component.StolenEssence.Float(), filterlistings));
|
||||
}
|
||||
}
|
||||
184
Content.Server/Revenant/EntitySystems/RevenantSystem.cs
Normal file
184
Content.Server/Revenant/EntitySystems/RevenantSystem.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Revenant;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Server.MobState;
|
||||
using Content.Server.Visible;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
|
||||
namespace Content.Server.Revenant.EntitySystems;
|
||||
|
||||
public sealed partial class RevenantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ActionsSystem _action = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly DamageableSystem _damage = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly PolymorphableSystem _polymorphable = default!;
|
||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interact = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedStunSystem _stun = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RevenantComponent, ComponentStartup>(OnStartup);
|
||||
|
||||
SubscribeLocalEvent<RevenantComponent, DamageChangedEvent>(OnDamage);
|
||||
SubscribeLocalEvent<RevenantComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<RevenantComponent, StatusEffectAddedEvent>(OnStatusAdded);
|
||||
SubscribeLocalEvent<RevenantComponent, StatusEffectEndedEvent>(OnStatusEnded);
|
||||
|
||||
InitializeAbilities();
|
||||
InitializeShop();
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, RevenantComponent component, ComponentStartup args)
|
||||
{
|
||||
//update the icon
|
||||
ChangeEssenceAmount(uid, 0, component);
|
||||
|
||||
//default the visuals
|
||||
_appearance.SetData(uid, RevenantVisuals.Corporeal, false);
|
||||
_appearance.SetData(uid, RevenantVisuals.Harvesting, false);
|
||||
_appearance.SetData(uid, RevenantVisuals.Stunned, false);
|
||||
|
||||
//ghost vision
|
||||
if (TryComp(component.Owner, out EyeComponent? eye))
|
||||
eye.VisibilityMask |= (uint) (VisibilityFlags.Ghost);
|
||||
|
||||
//get all the abilities
|
||||
foreach (var listing in _proto.EnumeratePrototypes<RevenantStoreListingPrototype>())
|
||||
component.Listings.Add(listing, true);
|
||||
|
||||
var shopaction = new InstantAction(_proto.Index<InstantActionPrototype>("RevenantShop"));
|
||||
_action.AddAction(uid, shopaction, null);
|
||||
}
|
||||
|
||||
private void OnStatusAdded(EntityUid uid, RevenantComponent component, StatusEffectAddedEvent args)
|
||||
{
|
||||
if (args.Key == "Stun")
|
||||
_appearance.SetData(uid, RevenantVisuals.Stunned, true);
|
||||
}
|
||||
|
||||
private void OnStatusEnded(EntityUid uid, RevenantComponent component, StatusEffectEndedEvent args)
|
||||
{
|
||||
if (args.Key == "Stun")
|
||||
_appearance.SetData(uid, RevenantVisuals.Stunned, false);
|
||||
else if (args.Key == "Corporeal")
|
||||
_movement.RefreshMovementSpeedModifiers(uid);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, RevenantComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (args.Examiner == args.Examined)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("revenant-essence-amount",
|
||||
("current", component.Essence.Int()), ("max", component.EssenceRegenCap.Int())));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDamage(EntityUid uid, RevenantComponent component, DamageChangedEvent args)
|
||||
{
|
||||
if (!HasComp<CorporealComponent>(uid) || args.DamageDelta == null)
|
||||
return;
|
||||
|
||||
var essenceDamage = args.DamageDelta.Total.Float() * component.DamageToEssenceCoefficient * -1;
|
||||
ChangeEssenceAmount(uid, essenceDamage, component);
|
||||
}
|
||||
|
||||
public bool ChangeEssenceAmount(EntityUid uid, FixedPoint2 amount, RevenantComponent? component = null, bool allowDeath = true, bool regenCap = false)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
if (!allowDeath && component.Essence + amount <= 0)
|
||||
return false;
|
||||
|
||||
component.Essence += amount;
|
||||
|
||||
if (regenCap)
|
||||
FixedPoint2.Min(component.Essence, component.EssenceRegenCap);
|
||||
|
||||
_alerts.ShowAlert(uid, AlertType.Essence, (short) Math.Clamp(Math.Round(component.Essence.Float() / 10f), 0, 16));
|
||||
|
||||
if (component.Essence <= 0)
|
||||
{
|
||||
component.Essence = component.EssenceRegenCap;
|
||||
_polymorphable.PolymorphEntity(uid, "Ectoplasm");
|
||||
}
|
||||
|
||||
UpdateUserInterface(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryUseAbility(EntityUid uid, RevenantComponent component, FixedPoint2 abilityCost, Vector2 debuffs)
|
||||
{
|
||||
if (component.Essence <= abilityCost)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-not-enough-essence"), uid, Filter.Entities(uid));
|
||||
return false;
|
||||
}
|
||||
|
||||
var tileref = Transform(uid).Coordinates.GetTileRef();
|
||||
if (tileref != null)
|
||||
{
|
||||
if(_physics.GetEntitiesIntersectingBody(uid, (int) CollisionGroup.Impassable).Count > 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("revenant-in-solid"), uid, Filter.Entities(uid));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ChangeEssenceAmount(uid, abilityCost, component, false);
|
||||
|
||||
_statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
|
||||
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var rev in EntityQuery<RevenantComponent>())
|
||||
{
|
||||
rev.Accumulator += frameTime;
|
||||
|
||||
if (rev.Accumulator <= 1)
|
||||
continue;
|
||||
rev.Accumulator -= 1;
|
||||
|
||||
if (rev.Essence < rev.EssenceRegenCap)
|
||||
{
|
||||
ChangeEssenceAmount(rev.Owner, rev.EssencePerSecond, rev, regenCap: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Content.Server/Revenant/EssenceComponent.cs
Normal file
25
Content.Server/Revenant/EssenceComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Content.Server.Revenant;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class EssenceComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the entity has been harvested yet.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool Harvested = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not a revenant has searched this entity
|
||||
/// for its soul yet.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool SearchComplete = false;
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of Essence that the entity has.
|
||||
/// Changes based on mob state.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float EssenceAmount = 0f;
|
||||
}
|
||||
220
Content.Server/Revenant/RevenantComponent.cs
Normal file
220
Content.Server/Revenant/RevenantComponent.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.Revenant;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using System.Threading;
|
||||
using Content.Shared.FixedPoint;
|
||||
|
||||
namespace Content.Server.Revenant;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class RevenantComponent : SharedRevenantComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The total amount of Essence the revenant has. Functions
|
||||
/// as health and is regenerated.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public FixedPoint2 Essence = 75;
|
||||
|
||||
/// <summary>
|
||||
/// Used for purchasing shop items.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public FixedPoint2 StolenEssence = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The entity's current max amount of essence. Can be increased
|
||||
/// through harvesting player souls.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("maxEssence")]
|
||||
public FixedPoint2 EssenceRegenCap = 75;
|
||||
|
||||
/// <summary>
|
||||
/// The coefficient of damage taken to actual health lost.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("damageToEssenceCoefficient")]
|
||||
public float DamageToEssenceCoefficient = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of essence passively generated per second.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("essencePerSecond")]
|
||||
public FixedPoint2 EssencePerSecond = 0.25f;
|
||||
|
||||
[ViewVariables]
|
||||
public float Accumulator = 0;
|
||||
|
||||
// Here's the gist of the harvest ability:
|
||||
// Step 1: The revenant clicks on an entity to "search" for it's soul, which creates a doafter.
|
||||
// Step 2: After the doafter is completed, the soul is "found" and can be harvested.
|
||||
// Step 3: Clicking the entity again begins to harvest the soul, which causes the revenant to become vulnerable
|
||||
// Step 4: The second doafter for the harvest completes, killing the target and granting the revenant essence.
|
||||
#region Harvest Ability
|
||||
/// <summary>
|
||||
/// The duration of the soul search
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("soulSearchDuration")]
|
||||
public float SoulSearchDuration = 2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects applied after the ability
|
||||
/// the first float corresponds to amount of time the entity is stunned.
|
||||
/// the second corresponds to the amount of time the entity is made solid.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("harvestDebuffs")]
|
||||
public Vector2 HarvestDebuffs = (5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The amount that is given to the revenant each time it's max essence is upgraded.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("maxEssenceUpgradeAmount")]
|
||||
public float MaxEssenceUpgradeAmount = 10;
|
||||
|
||||
public CancellationTokenSource? SoulSearchCancelToken;
|
||||
public CancellationTokenSource? HarvestCancelToken;
|
||||
#endregion
|
||||
|
||||
//In the nearby radius, causes various objects to be thrown, messed with, and containers opened
|
||||
//Generally just causes a mess
|
||||
#region Defile Ability
|
||||
/// <summary>
|
||||
/// The amount of essence that is needed to use the ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("defileCost")]
|
||||
public FixedPoint2 DefileCost = -30;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects applied after the ability
|
||||
/// the first float corresponds to amount of time the entity is stunned.
|
||||
/// the second corresponds to the amount of time the entity is made solid.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("defileDebuffs")]
|
||||
public Vector2 DefileDebuffs = (1, 4);
|
||||
|
||||
/// <summary>
|
||||
/// The radius around the user that this ability affects
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("defileRadius")]
|
||||
public float DefileRadius = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of tiles that are uprooted by the ability
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("defileTilePryAmount")]
|
||||
public int DefileTilePryAmount = 15;
|
||||
|
||||
/// <summary>
|
||||
/// The chance that an individual entity will have any of the effects
|
||||
/// happen to it.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("defileEffectChance")]
|
||||
public float DefileEffectChance = 0.5f;
|
||||
#endregion
|
||||
|
||||
#region Overload Lights Ability
|
||||
/// <summary>
|
||||
/// The amount of essence that is needed to use the ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("overloadCost")]
|
||||
public FixedPoint2 OverloadCost = -40;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects applied after the ability
|
||||
/// the first float corresponds to amount of time the entity is stunned.
|
||||
/// the second corresponds to the amount of time the entity is made solid.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("overloadDebuffs")]
|
||||
public Vector2 OverloadDebuffs = (3, 8);
|
||||
|
||||
/// <summary>
|
||||
/// The radius around the user that this ability affects
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("overloadRadius")]
|
||||
public float OverloadRadius = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The chance that each light in the radius of the ability will break and explode.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("overloadBreakChance")]
|
||||
public float OverloadBreakChance = 0.5f;
|
||||
#endregion
|
||||
|
||||
#region Blight Ability
|
||||
/// <summary>
|
||||
/// The amount of essence that is needed to use the ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("blightCost")]
|
||||
public float BlightCost = -50;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects applied after the ability
|
||||
/// the first float corresponds to amount of time the entity is stunned.
|
||||
/// the second corresponds to the amount of time the entity is made solid.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("blightDebuffs")]
|
||||
public Vector2 BlightDebuffs = (2, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The radius around the user that this ability affects
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("blightRadius")]
|
||||
public float BlightRadius = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The disease that is given to the victims of the ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("blightDiseasePrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
public string BlightDiseasePrototypeId = "SpectralTiredness";
|
||||
#endregion
|
||||
|
||||
#region Malfunction Ability
|
||||
/// <summary>
|
||||
/// The amount of essence that is needed to use the ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("malfunctionCost")]
|
||||
public FixedPoint2 MalfunctionCost = -60;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects applied after the ability
|
||||
/// the first float corresponds to amount of time the entity is stunned.
|
||||
/// the second corresponds to the amount of time the entity is made solid.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("malfunctionDebuffs")]
|
||||
public Vector2 MalfunctionDebuffs = (2, 8);
|
||||
|
||||
/// <summary>
|
||||
/// The radius around the user that this ability affects
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("malfunctionRadius")]
|
||||
public float MalfunctionRadius = 3.5f;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Stores all of the currently unlockable abilities in the shop.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Dictionary<RevenantStoreListingPrototype, bool> Listings = new ();
|
||||
}
|
||||
|
||||
public sealed class SoulSearchDoAfterComplete : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public SoulSearchDoAfterComplete(EntityUid target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HarvestDoAfterComplete : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public HarvestDoAfterComplete(EntityUid target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HarvestDoAfterCancelled : EntityEventArgs { }
|
||||
Reference in New Issue
Block a user