using Content.Server.Bible.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Popups; using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Bible; using Content.Shared.Damage.Systems; using Content.Shared.Ghost.Roles.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Timing; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Random; namespace Content.Server.Bible { public sealed class BibleSystem : EntitySystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly InventorySystem _invSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly UseDelaySystem _delay = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent>(AddSummonVerb); SubscribeLocalEvent(GetSummonAction); SubscribeLocalEvent(OnSummon); SubscribeLocalEvent(OnFamiliarDeath); SubscribeLocalEvent(OnSpawned); } private readonly Queue _addQueue = new(); private readonly Queue _remQueue = new(); /// /// This handles familiar respawning. /// public override void Update(float frameTime) { base.Update(frameTime); foreach (var entity in _addQueue) { EnsureComp(entity); } _addQueue.Clear(); foreach (var entity in _remQueue) { RemComp(entity); } _remQueue.Clear(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var _, out var summonableComp)) { summonableComp.Accumulator += frameTime; if (summonableComp.Accumulator < summonableComp.RespawnTime) { continue; } // Clean up the old body if (summonableComp.Summon != null) { Del(summonableComp.Summon.Value); summonableComp.Summon = null; } summonableComp.AlreadySummoned = false; _popupSystem.PopupEntity(Loc.GetString("bible-summon-respawn-ready", ("book", uid)), uid, PopupType.Medium); _audio.PlayPvs(summonableComp.SummonSound, uid); // Clean up the accumulator and respawn tracking component summonableComp.Accumulator = 0; _remQueue.Enqueue(uid); } } private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInteractEvent args) { if (!args.CanReach) return; if (!TryComp(uid, out UseDelayComponent? useDelay) || _delay.IsDelayed((uid, useDelay))) return; if (args.Target == null || args.Target == args.User || !_mobStateSystem.IsAlive(args.Target.Value)) { return; } if (!HasComp(args.User)) { _popupSystem.PopupEntity(Loc.GetString("bible-sizzle"), args.User, args.User); _audio.PlayPvs(component.SizzleSoundPath, args.User); _damageableSystem.TryChangeDamage(args.User, component.DamageOnUntrainedUse, true, origin: uid); _delay.TryResetDelay((uid, useDelay)); return; } // This only has a chance to fail if the target is not wearing anything on their head and is not a familiar. if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp(args.Target.Value)) { if (_random.Prob(component.FailChance)) { var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(selfFailMessage, args.User, args.User, PopupType.MediumCaution); _audio.PlayPvs(component.BibleHitSound, args.User); _damageableSystem.TryChangeDamage(args.Target.Value, component.DamageOnFail, true, origin: uid); _delay.TryResetDelay((uid, useDelay)); return; } } if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid)) { var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); } else { var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid)); _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); _audio.PlayPvs(component.HealSoundPath, args.User); _delay.TryResetDelay((uid, useDelay)); } } private void AddSummonVerb(EntityUid uid, SummonableComponent component, GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess || component.AlreadySummoned || component.SpecialItemPrototype == null) return; if (component.RequiresBibleUser && !HasComp(args.User)) return; AlternativeVerb verb = new() { Act = () => { if (!TryComp(args.User, out TransformComponent? userXform)) return; AttemptSummon((uid, component), args.User, userXform); }, Text = Loc.GetString("bible-summon-verb"), Priority = 2 }; args.Verbs.Add(verb); } private void GetSummonAction(EntityUid uid, SummonableComponent component, GetItemActionsEvent args) { if (component.AlreadySummoned) return; args.AddAction(ref component.SummonActionEntity, component.SummonAction); } private void OnSummon(Entity ent, ref SummonActionEvent args) { AttemptSummon(ent, args.Performer, Transform(args.Performer)); } /// /// Starts up the respawn stuff when /// the chaplain's familiar dies. /// private void OnFamiliarDeath(EntityUid uid, FamiliarComponent component, MobStateChangedEvent args) { if (args.NewMobState != MobState.Dead || component.Source == null) return; var source = component.Source; if (source != null && HasComp(source)) { _addQueue.Enqueue(source.Value); } } /// /// When the familiar spawns, set its source to the bible. /// private void OnSpawned(EntityUid uid, FamiliarComponent component, GhostRoleSpawnerUsedEvent args) { var parent = Transform(args.Spawner).ParentUid; if (!TryComp(parent, out var summonable)) return; component.Source = parent; summonable.Summon = uid; } private void AttemptSummon(Entity ent, EntityUid user, TransformComponent? position) { var (uid, component) = ent; if (component.AlreadySummoned || component.SpecialItemPrototype == null) return; if (component.RequiresBibleUser && !HasComp(user)) return; if (!Resolve(user, ref position)) return; if (component.Deleted || Deleted(uid)) return; if (!_blocker.CanInteract(user, uid)) return; // Make this familiar the component's summon var familiar = Spawn(component.SpecialItemPrototype, position.Coordinates); component.Summon = familiar; // If this is going to use a ghost role mob spawner, attach it to the bible. if (HasComp(familiar)) { _popupSystem.PopupEntity(Loc.GetString("bible-summon-requested"), user, user, PopupType.Medium); _transform.SetParent(familiar, uid); } component.AlreadySummoned = true; _actionsSystem.RemoveAction(user, component.SummonActionEntity); } } }