using Content.Server.Actions;
using Content.Server.Atmos.Components;
using Content.Server.Humanoid;
using Content.Server.Language;
using Content.Server.Objectives.Systems;
using Content.Server.Popups;
using Content.Server.Storage.EntitySystems;
using Content.Server.Stunnable;
using Content.Shared._EE.Shadowling.Systems;
using Content.Shared._EE.Shadowling;
using Content.Shared._EE.Shadowling.Components;
using Content.Shared._Goobstation.Flashbang;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Random;
namespace Content.Server._EE.Shadowling;
///
/// This handles the Shadowling's System
///
public sealed partial class ShadowlingSystem : SharedShadowlingSystem
{
// If you want to port this to another non-EE server then:
// * Remove language and ShadowlingChatSystem
// * Replace the announcer system in ascension egg
// * Add the anti-mind control device to another locker other than mantis
// * Remove the psionic insulation check in enthralling
//
// This is all shitcode. It always has been.
//
// Actions exist in their own systems, refer to the components
// This system handles shadowling interactions with entities and basic action-related functions
//
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly LanguageSystem _language = default!;
[Dependency] private readonly HumanoidAppearanceSystem _appearance = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(BeforeDamageChanged);
SubscribeLocalEvent(OnMobStateChanged);
SubscribeLocalEvent(OnFlashBanged);
SubscribeLocalEvent(OnDamageModify);
SubscribeLocalEvent(BeforeGunShot);
SubscribeLocalEvent(OnExamined);
SubscribeAbilities();
}
#region Event Handlers
private void OnMobStateChanged(EntityUid uid, ShadowlingComponent component, MobStateChangedEvent args)
{
// Remove all Thralls if shadowling is dead
if (args.NewMobState == MobState.Dead || args.NewMobState == MobState.Invalid)
{
if (component.CurrentPhase == ShadowlingPhases.Ascension)
return;
foreach (var thrall in component.Thralls)
{
_popup.PopupEntity(Loc.GetString("shadowling-dead"), thrall, thrall, PopupType.LargeCaution);
RemCompDeferred(thrall);
}
var ev = new ShadowlingDeathEvent();
RaiseLocalEvent(ev);
}
}
private void OnDamageModify(EntityUid uid, ShadowlingComponent component, DamageModifyEvent args)
{
foreach (var (key,_) in args.Damage.DamageDict)
{
if (key == "Heat")
args.Damage += component.HeatDamageProjectileModifier;
}
}
private void OnFlashBanged(EntityUid uid, ShadowlingComponent component, GetFlashbangedEvent args)
{
// Shadowling get damaged from flashbangs
if (!TryComp(uid, out var damageableComp))
return;
_damageable.TryChangeDamage(uid, component.HeatDamage, damageable: damageableComp);
}
public void OnThrallAdded(EntityUid uid, EntityUid thrall, ShadowlingComponent comp)
{
if (!TryComp(uid, out var lightDet))
return;
if (lightDet == null)
return;
lightDet.ResistanceModifier += comp.LightResistanceModifier;
}
public void OnThrallRemoved(EntityUid uid, EntityUid thrall, ShadowlingComponent comp)
{
if (!TryComp(uid, out var lightDet))
return;
if (lightDet == null)
return;
lightDet.ResistanceModifier -= comp.LightResistanceModifier;
}
private void OnInit(EntityUid uid, ShadowlingComponent component, ref ComponentInit args)
{
_language.AddLanguage(uid, component.SlingLanguageId);
if (!TryComp(uid, out ActionsComponent? actions))
return;
_actions.AddAction(uid, ref component.ActionHatchEntity, component.ActionHatch, component: actions);
}
private void BeforeDamageChanged(EntityUid uid, ShadowlingComponent comp, BeforeDamageChangedEvent args)
{
// Can't take damage during hatching
if (comp.IsHatching)
args.Cancelled = true;
}
public void OnPhaseChanged(EntityUid uid, ShadowlingComponent component, ShadowlingPhases phase)
{
if (!TryComp(uid, out var actions))
return;
if (phase == ShadowlingPhases.PostHatch)
{
Dirty(uid, component);
// When the entity gets polymorphed, the OnInit starts so... We have to remove it again here.
_actions.RemoveAction(uid, component.ActionHatchEntity);
AddPostHatchActions(uid, component);
EnsureComp(uid);
var lightMod = EnsureComp(uid);
lightMod.ResistanceModifier = 0.5f; // Let them start with 50% resistance, and decrease it per Thrall
}
else if (phase == ShadowlingPhases.Ascension)
{
// Remove all previous actions
foreach (var action in actions.Actions)
{
if (!HasComp(action))
continue;
_actions.RemoveAction(uid, action);
}
var ev = new ShadowlingAscendEvent();
RaiseLocalEvent(ev);
_codeCondition.SetCompleted(uid, component.ObjectiveAscend);
EnsureComp(uid);
AddComp(uid);
AddComp(uid);
AddComp(uid);
AddComp(uid);
_actions.AddAction(uid, ref component.ActionAnnihilateEntity, component.ActionAnnihilate, component: actions);
// For the future reader: uncomment if you want this in the game. Thralls turn into nightmares, so no need for it
//_actions.AddAction(uid, ref component.ActionHypnosisEntity, component.ActionHypnosis, component: actions);
_actions.AddAction(uid, ref component.ActionPlaneShiftEntity, component.ActionPlaneShift, component: actions);
_actions.AddAction(uid, ref component.ActionLightningStormEntity, component.ActionLightningStorm, component: actions);
_actions.AddAction(uid, ref component.ActionBroadcastEntity, component.ActionBroadcast, component: actions);
}
else if (phase == ShadowlingPhases.FailedAscension)
{
// git gud bro :sob: :pray:
foreach (var action in actions.Actions)
{
if (!HasComp(action))
continue;
_actions.RemoveAction(uid, action);
}
EnsureComp(uid);
_appearance.AddMarking(uid, "AbominationTorso");
_appearance.AddMarking(uid, "AbominationHorns");
}
}
private void BeforeGunShot(Entity ent, ref SelfBeforeGunShotEvent args)
{
// Slings cant shoot guns
if (args.Gun.Comp.ClumsyProof)
return;
if (!_random.Prob(0.5f))
return;
_damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent);
_stun.TryParalyze(ent, ent.Comp.GunShootFailStunTime, false);
args.Cancel();
}
private void OnExamined(EntityUid uid, ShadowlingComponent comp, ExaminedEvent args)
{
if (args.Examiner != uid)
return;
if (!TryComp(uid, out var lightDet))
return;
args.PushMarkup($"[color=#D22B2B]You take {lightDet.ResistanceModifier * lightDet.DamageToDeal.GetTotal()} burn damage from light[/color]");
}
#endregion
private void AddPostHatchActions(EntityUid uid, ShadowlingComponent component)
{
if (!TryComp(uid, out ActionsComponent? actions))
return;
// Le Comps
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
EnsureComp(uid);
_actions.AddAction(uid, ref component.ActionGlareEntity, component.ActionGlare, component: actions);
_actions.AddAction(uid, ref component.ActionEnthrallEntity, component.ActionEnthrall, component: actions);
_actions.AddAction(uid, ref component.ActionVeilEntity, component.ActionVeil, component: actions);
_actions.AddAction(uid, ref component.ActionRapidRehatchEntity, component.ActionRapidRehatch, component: actions);
_actions.AddAction(uid, ref component.ActionShadowWalkEntity, component.ActionShadowWalk, component: actions);
_actions.AddAction(uid, ref component.ActionIcyVeinsEntity, component.ActionIcyVeins, component: actions);
_actions.AddAction(uid, ref component.ActionDestroyEnginesEntity, component.ActionDestroyEngines, component: actions);
_actions.AddAction(uid, ref component.ActionCollectiveMindEntity, component.ActionCollectiveMind, component: actions);
}
public bool CanEnthrall(EntityUid uid, EntityUid target)
{
if (HasComp(target))
{
_popup.PopupEntity(Loc.GetString("shadowling-enthrall-shadowling"), uid, uid, PopupType.SmallCaution);
return false;
}
if (HasComp(target))
{
_popup.PopupEntity(Loc.GetString("shadowling-enthrall-already-thrall"), uid, uid, PopupType.SmallCaution);
return false;
}
if (!HasComp(target))
{
_popup.PopupEntity(Loc.GetString("shadowling-enthrall-non-humanoid"), uid, uid, PopupType.SmallCaution);
return false;
}
// Psionic interaction
if (HasComp(target))
{
_popup.PopupEntity(Loc.GetString("shadowling-enthrall-psionic-insulated"), uid, uid, PopupType.SmallCaution);
return false;
}
// Target needs to be alive
if (TryComp(target, out var mobState))
{
if (_mobStateSystem.IsCritical(target, mobState) || _mobStateSystem.IsCritical(target, mobState))
{
_popup.PopupEntity(Loc.GetString("shadowling-enthrall-dead"), uid, uid, PopupType.SmallCaution);
return false;
}
}
return true;
}
public bool CanGlare(EntityUid target)
{
if (!HasComp(target))
return false;
if (HasComp(target))
return false;
if (HasComp(target))
return false;
return true;
}
public void DoEnthrall(EntityUid uid, SimpleDoAfterEvent args)
{
if (args.Cancelled)
return;
if (args.Args.Target is null)
return;
var target = args.Args.Target.Value;
var thrall = EnsureComp(target);
if (TryComp(uid, out var sling))
{
sling.Thralls.Add(target);
thrall.Converter = uid;
OnThrallAdded(uid, target, sling);
}
_audio.PlayPvs(
new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg"),
target,
AudioParams.Default);
}
}