From c58331c4f3fdc9ef2fb804c21d65f041726a6af3 Mon Sep 17 00:00:00 2001 From: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:03:36 +0000 Subject: [PATCH] Prediction + Refactor: BibleSytem (#44800) * Prediction * Update * Update * Fix * Rename file * Fix * Update * Fix * cleanup * EntityUid? Source * Revert "EntityUid? Source" This reverts commit c169e1f030b1fced6951b8026dda8f4e01571424. * ftl and fix PVS * Update * review * Update --------- Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> --- Content.Server/Bible/BibleSystem.cs | 256 --------------- .../Bible/Components/BibleComponent.cs | 63 ---- .../Bible/Components/BibleUserComponent.cs | 5 - .../Bible/Components/FamiliarComponent.cs | 17 - .../Bible/Components/SummonableComponent.cs | 52 ---- .../SummonableRespawningComponent.cs | 9 - .../Roles/Events/GhostRoleSpawnerUsedEvent.cs | 25 -- Content.Server/Ghost/Roles/GhostRoleSystem.cs | 3 +- Content.Server/Prayer/PrayerSystem.cs | 4 +- Content.Shared/Bible/BibleSystem.cs | 291 ++++++++++++++++++ .../Bible/Components/BibleComponent.cs | 119 +++++++ .../Bible/Components/BibleUserComponent.cs | 13 + .../Bible/Components/FamiliarComponent.cs | 19 ++ .../Bible/Components/SummonableComponent.cs | 91 ++++++ .../SummonableRespawningComponent.cs | 10 + Content.Shared/Bible/SummonActionEvent.cs | 8 +- .../Ghost/GhostRoleSpawnerUsedEvent.cs | 9 + .../Objects/Specific/Chapel/bibles.yml | 9 +- .../Prototypes/SoundCollections/bible.yml | 10 + 19 files changed, 575 insertions(+), 438 deletions(-) delete mode 100644 Content.Server/Bible/BibleSystem.cs delete mode 100644 Content.Server/Bible/Components/BibleComponent.cs delete mode 100644 Content.Server/Bible/Components/BibleUserComponent.cs delete mode 100644 Content.Server/Bible/Components/FamiliarComponent.cs delete mode 100644 Content.Server/Bible/Components/SummonableComponent.cs delete mode 100644 Content.Server/Bible/Components/SummonableRespawningComponent.cs delete mode 100644 Content.Server/Ghost/Roles/Events/GhostRoleSpawnerUsedEvent.cs create mode 100644 Content.Shared/Bible/BibleSystem.cs create mode 100644 Content.Shared/Bible/Components/BibleComponent.cs create mode 100644 Content.Shared/Bible/Components/BibleUserComponent.cs create mode 100644 Content.Shared/Bible/Components/FamiliarComponent.cs create mode 100644 Content.Shared/Bible/Components/SummonableComponent.cs create mode 100644 Content.Shared/Bible/Components/SummonableRespawningComponent.cs create mode 100644 Content.Shared/Ghost/GhostRoleSpawnerUsedEvent.cs diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs deleted file mode 100644 index ce435bca2c..0000000000 --- a/Content.Server/Bible/BibleSystem.cs +++ /dev/null @@ -1,256 +0,0 @@ -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.Map; -using Robust.Shared.Player; -using Robust.Shared.Random; - -namespace Content.Server.Bible -{ - public sealed partial class BibleSystem : EntitySystem - { - [Dependency] private IRobustRandom _random = default!; - [Dependency] private ActionBlockerSystem _blocker = default!; - [Dependency] private DamageableSystem _damageableSystem = default!; - [Dependency] private InventorySystem _invSystem = default!; - [Dependency] private MobStateSystem _mobStateSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - [Dependency] private SharedActionsSystem _actionsSystem = default!; - [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private UseDelaySystem _delay = default!; - [Dependency] private 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; - } - - var userEnt = Identity.Entity(args.User, EntityManager); - var targetEnt = Identity.Entity(args.Target.Value, EntityManager); - - // 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 _) && !HasComp(args.Target.Value)) - { - if (_random.Prob(component.FailChance)) - { - var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); - _popupSystem.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); - - var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", targetEnt), ("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; - } - } - - string othersMessage; - string selfMessage; - - if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid)) - { - othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); - selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", targetEnt), ("bible", uid)); - - _audio.PlayPvs(component.HealSoundPath, args.User); - _delay.TryResetDelay((uid, useDelay)); - - if (component.HealingLightEffect.HasValue) - Spawn(component.HealingLightEffect.Value, new EntityCoordinates(args.Target.Value, default)); - } - else - { - othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", userEnt), ("target", targetEnt), ("bible", uid)); - selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", targetEnt), ("bible", uid)); - } - - _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); - _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); - } - - 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); - } - } -} diff --git a/Content.Server/Bible/Components/BibleComponent.cs b/Content.Server/Bible/Components/BibleComponent.cs deleted file mode 100644 index af600158e9..0000000000 --- a/Content.Server/Bible/Components/BibleComponent.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; - -namespace Content.Server.Bible.Components -{ - [RegisterComponent] - public sealed partial class BibleComponent : Component - { - /// - /// Default sound when bible hits somebody. - /// - private static readonly ProtoId DefaultBibleHit = new("BibleHit"); - - /// - /// Sound to play when bible hits somebody. - /// - [DataField] - public SoundSpecifier BibleHitSound = new SoundCollectionSpecifier(DefaultBibleHit, AudioParams.Default.WithVolume(-4f)); - - /// - /// Damage that will be healed on a success - /// - [DataField("damage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier Damage = default!; - - /// - /// Damage that will be dealt on a failure - /// - [DataField("damageOnFail", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier DamageOnFail = default!; - - /// - /// Damage that will be dealt when a non-chaplain attempts to heal - /// - [DataField("damageOnUntrainedUse", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier DamageOnUntrainedUse = default!; - - /// - /// Chance the bible will fail to heal someone with no helmet - /// - [DataField("failChance")] - [ViewVariables(VVAccess.ReadWrite)] - public float FailChance = 0.34f; - - [DataField("sizzleSound")] - public SoundSpecifier SizzleSoundPath = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg"); - [DataField("healSound")] - public SoundSpecifier HealSoundPath = new SoundPathSpecifier("/Audio/Effects/holy.ogg"); - - [DataField("locPrefix")] - public string LocPrefix = "bible"; - - /// - /// A short light effect to display when successfully healing someone - /// - [DataField] - public EntProtoId? HealingLightEffect = "HolyLightEffect"; - } -} diff --git a/Content.Server/Bible/Components/BibleUserComponent.cs b/Content.Server/Bible/Components/BibleUserComponent.cs deleted file mode 100644 index 5948b8a42b..0000000000 --- a/Content.Server/Bible/Components/BibleUserComponent.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Content.Server.Bible.Components -{ - [RegisterComponent] - public sealed partial class BibleUserComponent : Component {} -} diff --git a/Content.Server/Bible/Components/FamiliarComponent.cs b/Content.Server/Bible/Components/FamiliarComponent.cs deleted file mode 100644 index 2811e77b86..0000000000 --- a/Content.Server/Bible/Components/FamiliarComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Content.Server.Bible.Components -{ - /// - /// This component is for the chaplain's familiars, and mostly - /// used to track their current state and to give a component to check for - /// if any special behavior is needed. - /// - [RegisterComponent] - public sealed partial class FamiliarComponent : Component - { - /// - /// The entity this familiar was summoned from. - /// - [ViewVariables] - public EntityUid? Source = null; - } -} diff --git a/Content.Server/Bible/Components/SummonableComponent.cs b/Content.Server/Bible/Components/SummonableComponent.cs deleted file mode 100644 index fa180c932e..0000000000 --- a/Content.Server/Bible/Components/SummonableComponent.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Bible.Components -{ - /// - /// This lets you summon a mob or item with an alternative verb on the item - /// - [RegisterComponent] - public sealed partial class SummonableComponent : Component - { - /// - /// Default sound to play when entity is summoned. - /// - private static readonly ProtoId DefaultSummonSound = new("Summon"); - - /// - /// Sound to play when entity is summoned. - /// - [DataField] - public SoundSpecifier SummonSound = new SoundCollectionSpecifier(DefaultSummonSound, AudioParams.Default.WithVolume(-4f)); - - /// - /// Used for a special item only the Chaplain can summon. Usually a mob, but supports regular items too. - /// - [DataField("specialItem", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? SpecialItemPrototype = null; - public bool AlreadySummoned = false; - - [DataField("requiresBibleUser")] - public bool RequiresBibleUser = true; - - /// - /// The specific creature this summoned, if the SpecialItemPrototype has a mobstate. - /// - [ViewVariables] - public EntityUid? Summon = null; - - [DataField("summonAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string SummonAction = "ActionBibleSummon"; - - [DataField("summonActionEntity")] - public EntityUid? SummonActionEntity; - - /// Used for respawning - [DataField("accumulator")] - public float Accumulator = 0f; - [DataField("respawnTime")] - public float RespawnTime = 180f; - } -} diff --git a/Content.Server/Bible/Components/SummonableRespawningComponent.cs b/Content.Server/Bible/Components/SummonableRespawningComponent.cs deleted file mode 100644 index e8ac0a6a81..0000000000 --- a/Content.Server/Bible/Components/SummonableRespawningComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Content.Server.Bible.Components -{ - /// - /// EntityQuery Tracking Component for Summonables that are counting up a respawn. - /// - [RegisterComponent] - public sealed partial class SummonableRespawningComponent : Component - {} -} diff --git a/Content.Server/Ghost/Roles/Events/GhostRoleSpawnerUsedEvent.cs b/Content.Server/Ghost/Roles/Events/GhostRoleSpawnerUsedEvent.cs deleted file mode 100644 index 8a9c0267e9..0000000000 --- a/Content.Server/Ghost/Roles/Events/GhostRoleSpawnerUsedEvent.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Content.Server.Ghost.Roles.Events -{ - /// - /// Raised on a spawned entity after they use a ghost role mob spawner. - /// - public sealed class GhostRoleSpawnerUsedEvent : EntityEventArgs - { - /// - /// The entity that spawned this. - /// - public EntityUid Spawner; - - /// - /// The entity spawned. - /// - public EntityUid Spawned; - - public GhostRoleSpawnerUsedEvent(EntityUid spawner, EntityUid spawned) - { - Spawner = spawner; - - Spawned = spawned; - } - } -} diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs index 1c72382f3c..4d7122fc83 100644 --- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs @@ -4,7 +4,6 @@ using Content.Server.Administration.Managers; using Content.Server.EUI; using Content.Server.GameTicking.Events; using Content.Server.Ghost.Roles.Components; -using Content.Server.Ghost.Roles.Events; using Content.Shared.Ghost.Roles.Raffles; using Content.Server.Ghost.Roles.UI; using Content.Shared.Administration; @@ -784,7 +783,7 @@ public sealed partial class GhostRoleSystem : EntitySystem var mob = Spawn(component.Prototype, spawnCoordinates.Value); var spawnedEvent = new GhostRoleSpawnerUsedEvent(uid, mob); - RaiseLocalEvent(mob, spawnedEvent); + RaiseLocalEvent(mob, ref spawnedEvent); if (ghostRole.MakeSentient) _mindSystem.MakeSentient(mob, ghostRole.AllowMovement, ghostRole.AllowSpeech); diff --git a/Content.Server/Prayer/PrayerSystem.cs b/Content.Server/Prayer/PrayerSystem.cs index 7b4c9cb4d4..23113e3293 100644 --- a/Content.Server/Prayer/PrayerSystem.cs +++ b/Content.Server/Prayer/PrayerSystem.cs @@ -1,6 +1,5 @@ using Content.Server.Administration; using Content.Server.Administration.Logs; -using Content.Server.Bible.Components; using Content.Server.Chat.Managers; using Content.Server.Popups; using Content.Shared.Database; @@ -10,6 +9,7 @@ using Content.Shared.Prayer; using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Player; +using Content.Shared.Bible.Components; namespace Content.Server.Prayer; /// @@ -48,7 +48,7 @@ public sealed partial class PrayerSystem : EntitySystem Icon = comp.VerbImage, Act = () => { - if (comp.BibleUserOnly && !TryComp(args.User, out var bibleUser)) + if (comp.BibleUserOnly && !HasComp(args.User)) { _popupSystem.PopupEntity(Loc.GetString("prayer-popup-notify-pray-locked"), uid, actor.PlayerSession, PopupType.Large); return; diff --git a/Content.Shared/Bible/BibleSystem.cs b/Content.Shared/Bible/BibleSystem.cs new file mode 100644 index 0000000000..ab4a9ea6f9 --- /dev/null +++ b/Content.Shared/Bible/BibleSystem.cs @@ -0,0 +1,291 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Bible.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Ghost; +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.Random.Helpers; +using Content.Shared.Timing; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Bible; + +/// +/// Handles bible healing on hit, and summoning/respawning of familiars via . +/// +public sealed partial class BibleSystem : EntitySystem +{ + [Dependency] private ActionBlockerSystem _blocker = default!; + [Dependency] private DamageableSystem _damageable = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private INetManager _net = default!; + [Dependency] private InventorySystem _inventory = default!; + [Dependency] private MobStateSystem _mobState = default!; + [Dependency] private SharedActionsSystem _actions = default!; + [Dependency] private SharedAudioSystem _audio = default!; + [Dependency] private SharedContainerSystem _container = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private UseDelaySystem _delay = default!; + + /// + /// This handles familiar respawning. + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var respawningComponent, out var summonableComp)) + { + if (summonableComp.RespawnTime > _timing.CurTime) + continue; + + // Delete old summoned entity. + if (Exists(summonableComp.SummonedEntity)) + PredictedDel(summonableComp.SummonedEntity); + + summonableComp.CanSummon = true; + Dirty(uid, summonableComp); + + // Re-add summon action. + if (summonableComp.SummonActionEntity is { } action + && _container.TryGetContainingContainer(uid, out var container)) + { + _actions.GrantContainedAction(container.Owner, uid, action); + } + + if (_net.IsServer) + { + _popup.PopupEntity(Loc.GetString(summonableComp.SummonRespawnReadyText, ("book", uid)), uid, PopupType.Medium); + _audio.PlayPvs(summonableComp.SummonSound, uid); + } + + RemCompDeferred(uid, respawningComponent); + } + } + + [SubscribeLocalEvent] + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (!args.CanReach) + return; + + if (!TryComp(ent, out var useDelay) || _delay.IsDelayed((ent, useDelay))) + return; + + if (args.Target == null || args.Target == args.User || !_mobState.IsAlive(args.Target.Value)) + return; + + var userEnt = Identity.Entity(args.User, EntityManager); + var targetEnt = Identity.Entity(args.Target.Value, EntityManager); + + // Sizzle user if they are not a Bible user. + if (!HasComp(args.User)) + { + _popup.PopupEntity(Loc.GetString(ent.Comp.SizzleText, ("user", userEnt), ("target", targetEnt), ("bible", ent)), args.User, args.User); + + _audio.PlayPredicted(ent.Comp.SizzleSound, ent, args.User); + _damageable.TryChangeDamage(args.User, ent.Comp.DamageOnUntrainedUse, true, origin: ent); + _delay.TryResetDelay((ent, 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 (!_inventory.TryGetSlotEntity(args.Target.Value, "head", out _) && !HasComp(args.Target.Value)) + { + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent)); + if (rand.Prob(ent.Comp.FailChance)) + { + var othersFailMessage = Loc.GetString(ent.Comp.HealFailOthersText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + _popup.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution); + + var selfFailMessage = Loc.GetString(ent.Comp.HealFailSelfText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + _popup.PopupEntity(selfFailMessage, args.User, args.User, PopupType.MediumCaution); + + _audio.PlayPredicted(ent.Comp.BibleHitSound, ent, args.User); + _damageable.TryChangeDamage(args.Target.Value, ent.Comp.DamageOnFail, true, origin: ent); + _delay.TryResetDelay((ent, useDelay)); + + return; + } + } + + string othersMessage; + string selfMessage; + + if (_damageable.TryChangeDamage(args.Target.Value, ent.Comp.Damage, true, origin: ent)) + { + othersMessage = Loc.GetString(ent.Comp.HealSuccessOthersText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + selfMessage = Loc.GetString(ent.Comp.HealSuccessSelfText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + + _audio.PlayPredicted(ent.Comp.HealSound, ent, args.User); + _delay.TryResetDelay((ent, useDelay)); + + if (ent.Comp.HealingLightEffect.HasValue) + PredictedSpawnAtPosition(ent.Comp.HealingLightEffect.Value, Transform(args.Target.Value).Coordinates); + } + else + { + othersMessage = Loc.GetString(ent.Comp.HealSuccessNoneOthersText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + selfMessage = Loc.GetString(ent.Comp.HealSuccessNoneSelfText, ("user", userEnt), ("target", targetEnt), ("bible", ent)); + } + + _popup.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium); + _popup.PopupEntity(selfMessage, args.User, args.User, PopupType.Large); + } + + [SubscribeLocalEvent] + private void AddSummonVerb(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || !CanSummon(ent, args.User)) + return; + + var user = args.User; + AlternativeVerb verb = new() + { + Act = () => + { + AttemptSummon(ent, user); + }, + Text = Loc.GetString(ent.Comp.SummonVerbText), + Priority = 2 + }; + args.Verbs.Add(verb); + } + + [SubscribeLocalEvent] + private void GetSummonAction(Entity ent, ref GetItemActionsEvent args) + { + if (!CanSummon(ent, args.User)) + return; + + args.AddAction(ref ent.Comp.SummonActionEntity, ent.Comp.SummonActionPrototype); + } + + [SubscribeLocalEvent] + private void OnSummon(Entity ent, ref SummonActionEvent args) + { + AttemptSummon(ent, args.Performer); + } + + [SubscribeLocalEvent] + private void OnFamiliarDeath(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead || ent.Comp.Source == null) + return; + + StartRespawnTimer(ent.Comp.Source.Value); + } + + [SubscribeLocalEvent] + private void OnFamiliarRemoved(Entity ent, ref ComponentRemove args) + { + if (!TryComp(ent.Comp.Source, out var summonable)) + return; + + // If the entity is no longer a familiar, then there’s no need to keep track of it anymore + summonable.SummonedEntity = null; + Dirty(ent.Comp.Source.Value, summonable); + + StartRespawnTimer((ent.Comp.Source.Value, summonable)); + } + + [SubscribeLocalEvent] + private void OnSummonableRemoved(Entity ent, ref ComponentRemove args) + { + if (!TryComp(ent.Comp.SummonedEntity, out var familiar)) + return; + + familiar.Source = null; + Dirty(ent.Comp.SummonedEntity.Value, familiar); + } + + /// + /// When the familiar spawns, set its source to the bible. + /// + [SubscribeLocalEvent] + private void OnSpawned(Entity ent, ref GhostRoleSpawnerUsedEvent args) + { + var parent = Transform(args.Spawner).ParentUid; + if (!TryComp(parent, out var summonable)) + return; + + ent.Comp.Source = parent; + Dirty(ent); + + summonable.SummonedEntity = ent; + Dirty(parent, summonable); + } + + /// + /// Starts up the respawn timer for the chaplain's familiar. + /// + private void StartRespawnTimer(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + EnsureComp(ent); + + ent.Comp.RespawnTime = _timing.CurTime + ent.Comp.RespawnCooldown; + Dirty(ent); + } + + /// + /// Checks whether the summonable entity can be summoned by the given user. + /// + private bool CanSummon(Entity ent, EntityUid user) + { + return ent.Comp.CanSummon + && ent.Comp.SummonEntityPrototype.HasValue + && (!ent.Comp.RequiresBibleUser || HasComp(user)); + } + + /// + /// Attempts to summon a new familiar. + /// + private void AttemptSummon(Entity ent, EntityUid user) + { + if (!CanSummon(ent, user)) + return; + + if (!_blocker.CanInteract(user, ent)) + return; + + // Make this familiar the component's summon + var familiar = PredictedSpawnAtPosition(ent.Comp.SummonEntityPrototype, Transform(user).Coordinates); + ent.Comp.SummonedEntity = familiar; + + // If this is going to use a ghost role mob spawner, attach it to the bible. + if (HasComp(familiar)) + { + _popup.PopupEntity(Loc.GetString(ent.Comp.SummonRequestedText), user, user, PopupType.Medium); + _transform.SetParent(familiar, ent); + } + else + { + EnsureComp(familiar, out var familiarComponent); + familiarComponent.Source = ent; + Dirty(familiar, familiarComponent); + } + + _actions.RemoveAction(user, ent.Comp.SummonActionEntity); + + ent.Comp.CanSummon = false; + Dirty(ent); + } +} diff --git a/Content.Shared/Bible/Components/BibleComponent.cs b/Content.Shared/Bible/Components/BibleComponent.cs new file mode 100644 index 0000000000..2e3ad8b2b6 --- /dev/null +++ b/Content.Shared/Bible/Components/BibleComponent.cs @@ -0,0 +1,119 @@ +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Bible.Components; + +/// +/// Marks an entity as Bible that heals somebody on interaction. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(BibleSystem))] +public sealed partial class BibleComponent : Component +{ + /// + /// Default sound when Bible hits somebody. + /// + private static readonly ProtoId DefaultBibleHitSound = new("BibleHit"); + + /// + /// Default sound when Bible fails to heal somebody. + /// + private static readonly ProtoId DefaultBibleSizzleSound = new("BibleSizzle"); + + /// + /// Default sound when Bible heals somebody. + /// + private static readonly ProtoId DefaultBibleHealSound = new("BibleHeal"); + + /// + /// Sound to play when Bible hits somebody. + /// + [DataField] + public SoundSpecifier BibleHitSound = new SoundCollectionSpecifier(DefaultBibleHitSound, AudioParams.Default.WithVolume(-4f)); + + /// + /// Sound to play when Bible fails to heal somebody. + /// + [DataField] + public SoundSpecifier SizzleSound = new SoundCollectionSpecifier(DefaultBibleSizzleSound); + + /// + /// Sound to play when Bible heals somebody. + /// + [DataField] + public SoundSpecifier HealSound = new SoundCollectionSpecifier(DefaultBibleHealSound); + + /// + /// Damage that will be healed on a success. + /// + [DataField(required: true)] + public DamageSpecifier Damage = default!; + + /// + /// Damage that will be dealt on a failure. + /// + [DataField(required: true)] + public DamageSpecifier DamageOnFail = default!; + + /// + /// Damage that will be dealt when a non-chaplain attempts to heal. + /// + [DataField(required: true)] + public DamageSpecifier DamageOnUntrainedUse = default!; + + /// + /// Chance theBbible will fail to heal someone with no helmet. + /// + [DataField] + public float FailChance = 0.34f; + + /// + /// Loc string played when a non-chaplain attempts to use the bible. + /// + [DataField] + public string SizzleText = "bible-sizzle"; + + /// + /// Loc string shown to others when the bible fails to heal the target. + /// + [DataField] + public string HealFailOthersText = "bible-heal-fail-others"; + + /// + /// Loc string shown to the user when the bible fails to heal the target. + /// + [DataField] + public string HealFailSelfText = "bible-heal-fail-self"; + + /// + /// Loc string shown to others when the bible successfully heals the target. + /// + [DataField] + public string HealSuccessOthersText = "bible-heal-success-others"; + + /// + /// Loc string shown to the user when the bible successfully heals the target. + /// + [DataField] + public string HealSuccessSelfText = "bible-heal-success-self"; + + /// + /// Loc string shown to others when the bible hits a target without wounds. + /// + [DataField] + public string HealSuccessNoneOthersText = "bible-heal-success-none-others"; + + /// + /// Loc string shown to the user when the bible hits a target without wounds. + /// + [DataField] + public string HealSuccessNoneSelfText = "bible-heal-success-none-self"; + + /// + /// A short light effect to display when successfully healing someone. + /// + [DataField] + public EntProtoId? HealingLightEffect = "HolyLightEffect"; +} diff --git a/Content.Shared/Bible/Components/BibleUserComponent.cs b/Content.Shared/Bible/Components/BibleUserComponent.cs new file mode 100644 index 0000000000..72e773a9c6 --- /dev/null +++ b/Content.Shared/Bible/Components/BibleUserComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Bible.Components; + +/// +/// Marks entity as bible user. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(BibleSystem))] +public sealed partial class BibleUserComponent : Component +{ + public override bool SendOnlyToOwner => true; +} diff --git a/Content.Shared/Bible/Components/FamiliarComponent.cs b/Content.Shared/Bible/Components/FamiliarComponent.cs new file mode 100644 index 0000000000..8988ac17f8 --- /dev/null +++ b/Content.Shared/Bible/Components/FamiliarComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Bible.Components; + +/// +/// This component is for the Bible user's familiars, and mostly +/// used to track their current state and to give a component to check for +/// if any special behavior is needed. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(BibleSystem))] +public sealed partial class FamiliarComponent : Component +{ + /// + /// The entity this familiar was summoned from. + /// + [ViewVariables, AutoNetworkedField] + public EntityUid? Source = null; +} diff --git a/Content.Shared/Bible/Components/SummonableComponent.cs b/Content.Shared/Bible/Components/SummonableComponent.cs new file mode 100644 index 0000000000..4bbfce469a --- /dev/null +++ b/Content.Shared/Bible/Components/SummonableComponent.cs @@ -0,0 +1,91 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Bible.Components; + +/// +/// This lets you summon a mob or item with an alternative verb on the item +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(BibleSystem))] +public sealed partial class SummonableComponent : Component +{ + /// + /// Default sound to play when entity is summoned. + /// + private static readonly ProtoId DefaultSummonSound = new("Summon"); + + /// + /// Sound to play when entity is summoned. + /// + [DataField] + public SoundSpecifier SummonSound = new SoundCollectionSpecifier(DefaultSummonSound, AudioParams.Default.WithVolume(-4f)); + + /// + /// Used for a special item only the Chaplain can summon. Usually a mob, but supports regular items too. + /// + [DataField("summonEntity")] + public EntProtoId? SummonEntityPrototype; + + /// + /// The summon action for component to use. + /// + [DataField("summonAction")] + public EntProtoId SummonActionPrototype = "ActionBibleSummon"; + + /// + /// The specific creature this summoned, if the SpecialItemPrototype has a mobstate. + /// + [ViewVariables, AutoNetworkedField] + public EntityUid? SummonedEntity = null; + + /// + /// The created summon action. + /// + [DataField, AutoNetworkedField] + public EntityUid? SummonActionEntity; + + /// + /// Whether or not user must have . + /// + [DataField] + public bool RequiresBibleUser = true; + + /// + /// Whether or not summoning is allowed at the moment. + /// + [ViewVariables, AutoNetworkedField] + public bool CanSummon = true; + + /// + /// Loc string for the summon alt-verb text. + /// + [DataField] + public string SummonVerbText = "bible-summon-verb"; + + /// + /// Loc string shown when a summon has been requested and is awaiting a soul. + /// + [DataField] + public string SummonRequestedText = "bible-summon-requested"; + + /// + /// Loc string shown when the familiar has respawned and is ready to summon again. + /// + [DataField] + public string SummonRespawnReadyText = "bible-summon-respawn-ready"; + + /// + /// Cooldown before being able to summon a new entity. + /// + [DataField] + public TimeSpan RespawnCooldown = TimeSpan.FromSeconds(180); + + /// + /// Time after which summoning will be allowed. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan RespawnTime = TimeSpan.Zero; +} diff --git a/Content.Shared/Bible/Components/SummonableRespawningComponent.cs b/Content.Shared/Bible/Components/SummonableRespawningComponent.cs new file mode 100644 index 0000000000..ebbf27023b --- /dev/null +++ b/Content.Shared/Bible/Components/SummonableRespawningComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Bible.Components; + +/// +/// EntityQuery tracking component for summonables that are waiting for a respawn. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(BibleSystem))] +public sealed partial class SummonableRespawningComponent : Component; diff --git a/Content.Shared/Bible/SummonActionEvent.cs b/Content.Shared/Bible/SummonActionEvent.cs index 3d57f4095e..d20fe7040c 100644 --- a/Content.Shared/Bible/SummonActionEvent.cs +++ b/Content.Shared/Bible/SummonActionEvent.cs @@ -2,7 +2,7 @@ namespace Content.Shared.Bible; -public sealed partial class SummonActionEvent : InstantActionEvent -{ - -} \ No newline at end of file +/// +/// Action event for summoning. +/// +public sealed partial class SummonActionEvent : InstantActionEvent; diff --git a/Content.Shared/Ghost/GhostRoleSpawnerUsedEvent.cs b/Content.Shared/Ghost/GhostRoleSpawnerUsedEvent.cs new file mode 100644 index 0000000000..f106d377e7 --- /dev/null +++ b/Content.Shared/Ghost/GhostRoleSpawnerUsedEvent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Ghost; + +/// +/// Raised on a spawned entity after they use a ghost role mob spawner. +/// +/// The entity that spawned this. +/// The entity spawned. +[ByRefEvent] +public readonly record struct GhostRoleSpawnerUsedEvent(EntityUid Spawner, EntityUid Spawned); diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml index 2639dac99e..0f4e0a7442 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml @@ -34,7 +34,7 @@ - type: Prayable bibleUserOnly: true - type: Summonable - specialItem: SpawnPointGhostRemilia + summonEntity: SpawnPointGhostRemilia - type: ReactionMixer mixMessage: "bible-mixing-success" reactionTypes: @@ -81,11 +81,14 @@ types: Caustic: 50 failChance: 0 - locPrefix: "necro" + healFailOthersText: necro-heal-fail-others + healFailSelfText: necro-heal-fail-self + healSuccessOthersText: necro-heal-success-others + healSuccessSelfText: necro-heal-success-self healSound: "/Audio/Effects/lightburn.ogg" healingLightEffect: EvilLightEffect - type: Summonable - specialItem: SpawnPointGhostCerberus + summonEntity: SpawnPointGhostCerberus respawnTime: 300 - type: Sprite sprite: Objects/Specific/Chapel/necronomicon.rsi diff --git a/Resources/Prototypes/SoundCollections/bible.yml b/Resources/Prototypes/SoundCollections/bible.yml index 90075881cb..b22c38a68a 100644 --- a/Resources/Prototypes/SoundCollections/bible.yml +++ b/Resources/Prototypes/SoundCollections/bible.yml @@ -7,3 +7,13 @@ id: BibleHit files: - /Audio/Effects/hit_kick.ogg + +- type: soundCollection + id: BibleSizzle + files: + - /Audio/Effects/lightburn.ogg + +- type: soundCollection + id: BibleHeal + files: + - /Audio/Effects/holy.ogg