// SPDX-FileCopyrightText: 2025 GoobBot // SPDX-FileCopyrightText: 2025 Solstice // SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter // // SPDX-License-Identifier: AGPL-3.0-or-later using System.Text.RegularExpressions; using Content.Server._Goobstation.Devil.Condemned; using Content.Server._Goobstation.Devil.Contract; using Content.Server._Goobstation.Devil.Objectives.Components; using Content.Server._Goobstation.Possession; using Content.Shared._Goobstation.Bible; using Content.Shared._Goobstation.CheatDeath; using Content.Shared._Goobstation.CrematorImmune; using Content.Shared._Goobstation.Devil; using Content.Shared._Goobstation.Devil.Condemned; using Content.Shared._Goobstation.Exorcism; using Content.Shared._Goobstation.Religion; using Content.Server.Actions; using Content.Server.Administration.Systems; using Content.Server.Atmos.Components; using Content.Server.Bible.Components; using Content.Server.Body.Systems; using Content.Server.Chat.Systems; using Content.Server.Destructible; using Content.Server.Hands.Systems; using Content.Server.Jittering; using Content.Server.Mind; using Content.Server.Polymorph.Systems; using Content.Server.Popups; using Content.Server.Speech; using Content.Server.Speech.Components; using Content.Server.Stunnable; using Content.Server.Temperature.Components; using Content.Server.Zombies; using Content.Shared.Silicon.Components; using Content.Shared._Shitmed.Body.Components; using Content.Shared.Actions; using Content.Shared.CombatMode; using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; using Content.Shared.Polymorph; using Content.Shared.Popups; using Content.Shared.Silicon.Components; using Content.Shared.Temperature.Components; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server._Goobstation.Devil; public sealed partial class DevilSystem : EntitySystem { [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly PolymorphSystem _poly = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly RejuvenateSystem _rejuvenate = default!; [Dependency] private readonly DevilContractSystem _contract = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PossessionSystem _possession = default!; [Dependency] private readonly CondemnedSystem _condemned = default!; [Dependency] private readonly MobStateSystem _state = default!; [Dependency] private readonly JitteringSystem _jittering = default!; [Dependency] private readonly BodySystem _body = default!; private static readonly Regex WhitespaceAndNonWordRegex = new(@"[\s\W]+", RegexOptions.Compiled); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnListen); SubscribeLocalEvent(OnSoulAmountChanged); SubscribeLocalEvent(OnPowerLevelChanged); SubscribeLocalEvent(OnExorcismDoAfter); SubscribeLocalEvent>(OnEyesCoveredCheckEvent); InitializeHandshakeSystem(); SubscribeAbilities(); } #region Startup & Remove private void OnStartup(Entity devil, ref MapInitEvent args) { // Remove human components. RemComp(devil); RemComp(devil); RemComp(devil); RemComp(devil); RemComp(devil); RemComp(devil); RemComp(devil); // Adjust stats EnsureComp(devil); EnsureComp(devil); EnsureComp(devil); EnsureComp(devil); EnsureComp(devil).AlwaysTakeHoly = true; EnsureComp(devil); // Allow infinite revival var revival = EnsureComp(devil); revival.InfiniteRevives = true; revival.CanCheatStanding = true; // Change damage modifier if (TryComp(devil, out var damageableComp)) _damageable.SetDamageModifierSetId(devil, devil.Comp.DevilDamageModifierSet, damageableComp); // Add base actions foreach (var actionId in devil.Comp.BaseDevilActions) _actions.AddAction(devil, actionId); // Self Explanatory GenerateTrueName(devil); } #endregion #region Event Listeners private void OnSoulAmountChanged(Entity devil, ref SoulAmountChangedEvent args) { if (!_mind.TryGetMind(args.User, out var mindId, out var mind)) return; devil.Comp.Souls += args.Amount; _popup.PopupEntity(Loc.GetString("contract-soul-added"), args.User, args.User, PopupType.MediumCaution); if (devil.Comp.Souls is > 1 and < 7 && devil.Comp.Souls % 2 == 0) { devil.Comp.PowerLevel = (DevilPowerLevel)(devil.Comp.Souls / 2); // malicious casting to enum // Raise event var ev = new PowerLevelChangedEvent(args.User, devil.Comp.PowerLevel); RaiseLocalEvent(args.User, ref ev); } if (_mind.TryGetObjectiveComp(mindId, out var objectiveComp, mind)) objectiveComp.ContractsSigned += args.Amount; } private void OnPowerLevelChanged(Entity devil, ref PowerLevelChangedEvent args) { var popup = Loc.GetString($"devil-power-level-increase-{args.NewLevel.ToString().ToLowerInvariant()}"); _popup.PopupEntity(popup, args.User, args.User, PopupType.Large); if (!_prototype.TryIndex(devil.Comp.DevilBranchPrototype, out var proto)) return; foreach (var ability in proto.PowerActions) { if (args.NewLevel != ability.Key) continue; foreach (var actionId in ability.Value) _actions.AddAction(devil, actionId); } } private void OnExamined(Entity ent, ref ExaminedEvent args) { if (!args.IsInDetailsRange || ent.Comp.PowerLevel < DevilPowerLevel.Weak) return; var ev = new IsEyesCoveredCheckEvent(); RaiseLocalEvent(ent, ev); if (ev.IsEyesProtected) return; args.PushMarkup(Loc.GetString("devil-component-examined", ("target", Identity.Entity(ent, EntityManager)))); } private void OnEyesCoveredCheckEvent(Entity ent, ref InventoryRelayedEvent args) { if (ent.Comp.Enabled) args.Args.IsEyesProtected = true; } private void OnListen(Entity devil, ref ListenEvent args) { // Other Devils and entities without souls have no authority over you. if (HasComp(args.Source) || HasComp(args.Source) || HasComp(args.Source) || args.Source == devil.Owner) return; var message = WhitespaceAndNonWordRegex.Replace(args.Message.ToLowerInvariant(), ""); var trueName = WhitespaceAndNonWordRegex.Replace(devil.Comp.TrueName.ToLowerInvariant(), ""); if (!message.Contains(trueName)) return; // hardcoded, but this is just flavor so who cares :godo: _jittering.DoJitter(devil, TimeSpan.FromSeconds(4), true); if (_timing.CurTime < devil.Comp.LastTriggeredTime + devil.Comp.CooldownDuration) return; devil.Comp.LastTriggeredTime = _timing.CurTime; if (HasComp(args.Source)) { _damageable.TryChangeDamage(devil, devil.Comp.DamageOnTrueName * devil.Comp.BibleUserDamageMultiplier, true); _stun.TryParalyze(devil, devil.Comp.ParalyzeDurationOnTrueName * devil.Comp.BibleUserDamageMultiplier, false); var popup = Loc.GetString("devil-true-name-heard-chaplain", ("speaker", args.Source), ("target", devil)); _popup.PopupEntity(popup, devil, PopupType.LargeCaution); } else { _stun.TryParalyze(devil, devil.Comp.ParalyzeDurationOnTrueName, false); _damageable.TryChangeDamage(devil, devil.Comp.DamageOnTrueName, true); var popup = Loc.GetString("devil-true-name-heard", ("speaker", args.Source), ("target", devil)); _popup.PopupEntity(popup, devil, PopupType.LargeCaution); } } private void OnExorcismDoAfter(Entity devil, ref ExorcismDoAfterEvent args) { if (args.Target is not { } target || args.Cancelled || args.Handled) return; _popup.PopupEntity(Loc.GetString("devil-exorcised", ("target", devil.Comp.TrueName)), devil, PopupType.LargeCaution); _condemned.StartCondemnation(target, behavior: CondemnedBehavior.Banish, doFlavor: false); } #endregion #region Helper Methods private static bool TryUseAbility(BaseActionEvent action) { if (action.Handled) return false; action.Handled = true; return true; } private void PlayFwooshSound(EntityUid uid, DevilComponent? comp = null) { if (!Resolve(uid, ref comp)) return; _audio.PlayPvs(comp.FwooshPath, uid, new AudioParams(-2f, 1f, SharedAudioSystem.DefaultSoundRange, 1f, false, 0f)); } private void DoContractFlavor(EntityUid devil, string name) { var flavor = Loc.GetString("contract-summon-flavor", ("name", name)); _popup.PopupEntity(flavor, devil, PopupType.Medium); } private void GenerateTrueName(DevilComponent comp) { // Generate true name. var firstNameOptions = _prototype.Index(comp.FirstNameTrue); var lastNameOptions = _prototype.Index(comp.LastNameTrue); comp.TrueName = string.Concat(_random.Pick(firstNameOptions.Values), " ", _random.Pick(lastNameOptions.Values)); } #endregion }