Cocoon Cleanup & Minor Bloodsucker Tweaks (#1058)

# Description

- Generalizes cocooning
- Allows any mob to be cocooned
- Cocoon bloodsucking moved to vampirism system
- Any blood sucker can drink from cocoons
- Vampirism no longer fails if bloodstream isn't normal blood, but gives
a pop up
- Vampirism `WebRequired` actually works in a way that makes sense
- Adds cocooning and bloodsucker to all spider mobs + Arachnids

resolves #978

---

# Changelog

🆑
- tweak: All spiders, arachne, and arachnids can cocoon mobs, and drink
their blood.

---------

Co-authored-by: VMSolidus <evilexecutive@gmail.com>
This commit is contained in:
Aiden
2024-10-17 14:21:01 -05:00
committed by Remuchi
parent 5acf312fef
commit 2b10679f87
15 changed files with 85 additions and 182 deletions

View File

@@ -1,4 +1,4 @@
using Content.Shared.Arachne;
using Content.Shared.Cocoon;
using Content.Shared.Humanoid;
using Robust.Client.GameObjects;
using Robust.Shared.Containers;

View File

@@ -1,7 +1,6 @@
using Content.Shared.Arachne;
using Content.Shared.Cocoon;
using Content.Shared.IdentityManagement;
using Content.Shared.Verbs;
using Content.Shared.Buckle.Components;
using Content.Shared.DoAfter;
using Content.Shared.Stunnable;
using Content.Shared.Eye.Blinding.Systems;
@@ -10,29 +9,21 @@ using Content.Shared.Damage;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Server.Buckle.Systems;
using Content.Server.Popups;
using Content.Server.DoAfter;
using Content.Server.Body.Components;
using Content.Server.Vampiric;
using Content.Server.Speech.Components;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
using Robust.Server.Console;
using Content.Shared.Mobs.Components;
namespace Content.Server.Arachne
namespace Content.Server.Cocoon
{
public sealed class ArachneSystem : EntitySystem
public sealed class CocooningSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly BuckleSystem _buckleSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly BlindableSystem _blindableSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IServerConsoleHost _host = default!;
[Dependency] private readonly BloodSuckerSystem _bloodSuckerSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
private const string BodySlot = "body_slot";
@@ -40,27 +31,16 @@ namespace Content.Server.Arachne
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArachneComponent, GetVerbsEvent<InnateVerb>>(AddCocoonVerb);
SubscribeLocalEvent<CocoonerComponent, GetVerbsEvent<InnateVerb>>(AddCocoonVerb);
SubscribeLocalEvent<CocoonComponent, EntInsertedIntoContainerMessage>(OnCocEntInserted);
SubscribeLocalEvent<CocoonComponent, EntRemovedFromContainerMessage>(OnCocEntRemoved);
SubscribeLocalEvent<CocoonComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<CocoonComponent, GetVerbsEvent<AlternativeVerb>>(AddSuccVerb);
SubscribeLocalEvent<ArachneComponent, ArachneCocoonDoAfterEvent>(OnCocoonDoAfter);
SubscribeLocalEvent<CocoonerComponent, CocoonDoAfterEvent>(OnCocoonDoAfter);
}
private void AddCocoonVerb(EntityUid uid, ArachneComponent component, GetVerbsEvent<InnateVerb> args)
private void AddCocoonVerb(EntityUid uid, CocoonerComponent component, GetVerbsEvent<InnateVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (args.Target == uid)
return;
if (!TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
return;
if (bloodstream.BloodReagent != component.WebBloodReagent)
if (!args.CanAccess || !args.CanInteract || !HasComp<MobStateComponent>(args.Target))
return;
InnateVerb verb = new()
@@ -77,31 +57,23 @@ namespace Content.Server.Arachne
private void OnCocEntInserted(EntityUid uid, CocoonComponent component, EntInsertedIntoContainerMessage args)
{
_blindableSystem.UpdateIsBlind(args.Entity);
EnsureComp<StunnedComponent>(args.Entity);
component.Victim = args.Entity;
if (TryComp<ReplacementAccentComponent>(args.Entity, out var currentAccent))
{
component.WasReplacementAccent = true;
component.OldAccent = currentAccent.Accent;
currentAccent.Accent = "mumble";
} else
{
component.WasReplacementAccent = false;
var replacement = EnsureComp<ReplacementAccentComponent>(args.Entity);
replacement.Accent = "mumble";
}
EnsureComp<ReplacementAccentComponent>(args.Entity).Accent = "mumble";
EnsureComp<StunnedComponent>(args.Entity);
_blindableSystem.UpdateIsBlind(args.Entity);
}
private void OnCocEntRemoved(EntityUid uid, CocoonComponent component, EntRemovedFromContainerMessage args)
{
if (component.WasReplacementAccent && TryComp<ReplacementAccentComponent>(args.Entity, out var replacement))
{
replacement.Accent = component.OldAccent;
} else
{
if (TryComp<ReplacementAccentComponent>(args.Entity, out var replacement))
replacement.Accent = component.OldAccent ?? replacement.Accent;
else
RemComp<ReplacementAccentComponent>(args.Entity);
}
RemComp<StunnedComponent>(args.Entity);
_blindableSystem.UpdateIsBlind(args.Entity);
@@ -109,79 +81,24 @@ namespace Content.Server.Arachne
private void OnDamageChanged(EntityUid uid, CocoonComponent component, DamageChangedEvent args)
{
if (!args.DamageIncreased)
return;
if (args.DamageDelta == null)
return;
var body = _itemSlots.GetItemOrNull(uid, BodySlot);
if (body == null)
if (!args.DamageIncreased || args.DamageDelta == null || component.Victim == null)
return;
var damage = args.DamageDelta * component.DamagePassthrough;
_damageableSystem.TryChangeDamage(body, damage);
_damageableSystem.TryChangeDamage(component.Victim, damage);
}
private void AddSuccVerb(EntityUid uid, CocoonComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!TryComp<BloodSuckerComponent>(args.User, out var sucker))
return;
if (!sucker.WebRequired)
return;
var victim = _itemSlots.GetItemOrNull(uid, BodySlot);
if (victim == null)
return;
if (!TryComp<BloodstreamComponent>(victim, out var stream))
return;
AlternativeVerb verb = new()
{
Act = () =>
{
_bloodSuckerSystem.StartSuccDoAfter(args.User, victim.Value, sucker, stream, false); // start doafter
},
Text = Loc.GetString("action-name-suck-blood"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Nyanotrasen/Icons/verbiconfangs.png")),
Priority = 2
};
args.Verbs.Add(verb);
}
private void OnEntRemoved(EntityUid uid, WebComponent web, EntRemovedFromContainerMessage args)
{
if (!TryComp<StrapComponent>(uid, out var strap))
return;
if (HasComp<ArachneComponent>(args.Entity))
_buckleSystem.StrapSetEnabled(uid, false, strap);
}
private void StartCocooning(EntityUid uid, ArachneComponent component, EntityUid target)
private void StartCocooning(EntityUid uid, CocoonerComponent component, EntityUid target)
{
_popupSystem.PopupEntity(Loc.GetString("cocoon-start-third-person", ("target", Identity.Entity(target, EntityManager)), ("spider", Identity.Entity(uid, EntityManager))), uid,
Shared.Popups.PopupType.MediumCaution);
_popupSystem.PopupEntity(Loc.GetString("cocoon-start-second-person", ("target", Identity.Entity(target, EntityManager))), uid, uid, Shared.Popups.PopupType.Medium);
var delay = component.CocoonDelay;
if (HasComp<KnockedDownComponent>(target))
delay *= component.CocoonKnockdownMultiplier;
// Is it good practice to use empty data just to disambiguate doafters
// Who knows, there's no docs!
var ev = new ArachneCocoonDoAfterEvent();
var args = new DoAfterArgs(EntityManager, uid, delay, ev, uid, target: target)
var args = new DoAfterArgs(EntityManager, uid, delay, new CocoonDoAfterEvent(), uid, target: target)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
@@ -190,7 +107,7 @@ namespace Content.Server.Arachne
_doAfter.TryStartDoAfter(args);
}
private void OnCocoonDoAfter(EntityUid uid, ArachneComponent component, ArachneCocoonDoAfterEvent args)
private void OnCocoonDoAfter(EntityUid uid, CocoonerComponent component, CocoonDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;

View File

@@ -6,6 +6,7 @@ using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Administration.Logs;
using Content.Shared.Vampiric;
using Content.Shared.Cocoon;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
@@ -45,23 +46,28 @@ namespace Content.Server.Vampiric
private void AddSuccVerb(EntityUid uid, BloodSuckerComponent component, GetVerbsEvent<InnateVerb> args)
{
if (args.User == args.Target)
var victim = args.Target;
var ignoreClothes = false;
if (TryComp<CocoonComponent>(args.Target, out var cocoon))
{
victim = cocoon.Victim ?? args.Target;
ignoreClothes = cocoon.Victim != null;
} else if (component.WebRequired)
return;
if (component.WebRequired)
return; // handled elsewhere
if (!TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
return;
if (!args.CanAccess)
if (!TryComp<BloodstreamComponent>(victim, out var bloodstream) || args.User == victim || !args.CanAccess)
return;
InnateVerb verb = new()
{
Act = () =>
{
StartSuccDoAfter(uid, args.Target, component, bloodstream); // start doafter
StartSuccDoAfter(uid, victim, component, bloodstream, !ignoreClothes); // start doafter
},
Text = Loc.GetString("action-name-suck-blood"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Nyanotrasen/Icons/verbiconfangs.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Nyanotrasen/Icons/verbiconfangs.png")),
Priority = 2
};
args.Verbs.Add(verb);
@@ -80,10 +86,8 @@ namespace Content.Server.Vampiric
if (_prototypeManager.TryIndex<DamageGroupPrototype>("Brute", out var brute) && args.Damageable.Damage.TryGetDamageInGroup(brute, out var bruteTotal)
&& _prototypeManager.TryIndex<DamageGroupPrototype>("Airloss", out var airloss) && args.Damageable.Damage.TryGetDamageInGroup(airloss, out var airlossTotal))
{
if (bruteTotal == 0 && airlossTotal == 0)
RemComp<BloodSuckedComponent>(uid);
}
}
private void OnDoAfter(EntityUid uid, BloodSuckerComponent component, BloodSuckDoAfterEvent args)
@@ -96,18 +100,13 @@ namespace Content.Server.Vampiric
public void StartSuccDoAfter(EntityUid bloodsucker, EntityUid victim, BloodSuckerComponent? bloodSuckerComponent = null, BloodstreamComponent? stream = null, bool doChecks = true)
{
if (!Resolve(bloodsucker, ref bloodSuckerComponent))
return;
if (!Resolve(victim, ref stream))
if (!Resolve(bloodsucker, ref bloodSuckerComponent) || !Resolve(victim, ref stream))
return;
if (doChecks)
{
if (!_interactionSystem.InRangeUnobstructed(bloodsucker, victim))
{
return;
}
if (_inventorySystem.TryGetSlotEntity(victim, "head", out var headUid) && HasComp<PressureProtectionComponent>(headUid))
{
@@ -125,19 +124,15 @@ namespace Content.Server.Vampiric
}
if (stream.BloodReagent != "Blood")
{
_popups.PopupEntity(Loc.GetString("bloodsucker-fail-not-blood", ("target", victim)), victim, bloodsucker, Shared.Popups.PopupType.Medium);
return;
}
if (_solutionSystem.PercentFull(stream.Owner) != 0)
_popups.PopupEntity(Loc.GetString("bloodsucker-not-blood", ("target", victim)), victim, bloodsucker, Shared.Popups.PopupType.Medium);
else if (_solutionSystem.PercentFull(victim) != 0)
_popups.PopupEntity(Loc.GetString("bloodsucker-fail-no-blood", ("target", victim)), victim, bloodsucker, Shared.Popups.PopupType.Medium);
else
_popups.PopupEntity(Loc.GetString("bloodsucker-doafter-start", ("target", victim)), victim, bloodsucker, Shared.Popups.PopupType.Medium);
_popups.PopupEntity(Loc.GetString("bloodsucker-doafter-start-victim", ("sucker", bloodsucker)), victim, victim, Shared.Popups.PopupType.LargeCaution);
_popups.PopupEntity(Loc.GetString("bloodsucker-doafter-start", ("target", victim)), victim, bloodsucker, Shared.Popups.PopupType.Medium);
var ev = new BloodSuckDoAfterEvent();
var args = new DoAfterArgs(EntityManager, bloodsucker, bloodSuckerComponent.Delay, ev, bloodsucker, target: victim)
var args = new DoAfterArgs(EntityManager, bloodsucker, bloodSuckerComponent.Delay, new BloodSuckDoAfterEvent(), bloodsucker, target: victim)
{
BreakOnTargetMove = true,
BreakOnUserMove = false,
@@ -206,8 +201,5 @@ namespace Content.Server.Vampiric
//}
return true;
}
private record struct BloodSuckData()
{}
}
}

View File

@@ -1,21 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Arachne
{
[RegisterComponent, NetworkedComponent]
public sealed partial class ArachneComponent : Component
{
[DataField("cocoonDelay")]
public float CocoonDelay = 12f;
[DataField("cocoonKnockdownMultiplier")]
public float CocoonKnockdownMultiplier = 0.5f;
/// <summary>
/// Blood reagent required to web up a mob.
/// </summary>
[DataField("webBloodReagent")]
public string WebBloodReagent = "Blood";
}
}

View File

@@ -1,11 +0,0 @@
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Content.Shared.DoAfter;
namespace Content.Shared.Arachne
{
[Serializable, NetSerializable]
public sealed partial class ArachneCocoonDoAfterEvent : SimpleDoAfterEvent
{
}
}

View File

@@ -1,8 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Arachne
{
[RegisterComponent, NetworkedComponent]
public sealed partial class WebComponent : Component
{}
}

View File

@@ -1,13 +1,14 @@
namespace Content.Shared.Arachne
namespace Content.Shared.Cocoon
{
[RegisterComponent]
public sealed partial class CocoonComponent : Component
{
public bool WasReplacementAccent = false;
public string? OldAccent;
public string OldAccent = "";
public EntityUid? Victim;
[DataField("damagePassthrough")]
public float DamagePassthrough = 0.5f;
}
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Serialization;
using Content.Shared.DoAfter;
namespace Content.Shared.Cocoon
{
[Serializable, NetSerializable]
public sealed partial class CocoonDoAfterEvent : SimpleDoAfterEvent
{
}
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Cocoon
{
[RegisterComponent, NetworkedComponent]
public sealed partial class CocoonerComponent : Component
{
[DataField("cocoonDelay")]
public float CocoonDelay = 12f;
[DataField("cocoonKnockdownMultiplier")]
public float CocoonKnockdownMultiplier = 0.5f;
}
}

View File

@@ -4,9 +4,9 @@ action-description-suck-blood = Suck the blood of the victim in your hand.
bloodsucker-fail-helmet = You'd need to remove {THE($helmet)}.
bloodsucker-fail-mask = You'd need to remove your mask!
bloodsucker-fail-not-blood = { CAPITALIZE(SUBJECT($target)) } doesn't have delicious, nourishing mortal blood.
bloodsucker-fail-no-blood = { CAPITALIZE(SUBJECT($target)) } has no blood in { POSS-ADJ($target) } body.
bloodsucker-fail-no-blood-bloodsucked = { CAPITALIZE(SUBJECT($target)) } has been sucked dry.
bloodsucker-not-blood = {$target} doesn't have delicious, nourishing blood.
bloodsucker-fail-no-blood = {$target} has no blood in { POSS-ADJ($target) } body.
bloodsucker-fail-no-blood-bloodsucked = {$target} has been sucked dry.
bloodsucker-blood-sucked = You suck some blood from {$target}.
bloodsucker-doafter-start = You try to suck blood from {$target}.

View File

@@ -2357,6 +2357,9 @@
- type: RandomBark
barkType: hissing
barkMultiplier: 0.3
- type: BloodSucker
webRequired: true
- type: Cocooner
- type: entity
name: tarantula

View File

@@ -277,6 +277,9 @@
Unsexed: UnisexArachnid
- type: TypingIndicator
proto: spider
- type: BloodSucker
webRequired: true
- type: Cocooner
- type: entity
id: MobSpiderSpaceSalvage

View File

@@ -111,7 +111,7 @@
bloodRegenerationThirst: 4 # 1 unit of demon's blood satiates 4 thirst
- type: BloodSucker
webRequired: true
- type: Arachne
- type: Cocooner
- type: DamageVisuals
targetLayers:
- "enum.HumanoidVisualLayers.Chest"

View File

@@ -44,6 +44,9 @@
methods: [Touch]
effects:
- !type:WashCreamPieReaction
- type: BloodSucker
webRequired: true
- type: Cocooner
# Damage (Self)
- type: Bloodstream
bloodReagent: CopperBlood

View File

@@ -109,7 +109,7 @@
# path: /Audio/Animals/snake_hiss.ogg
# - type: Puller
# needsHands: false
# - type: Arachne
# - type: Cocooner
# cocoonDelay: 8
# - type: SolutionContainerManager
# solutions: