diff --git a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs new file mode 100644 index 00000000000..5d011ccbddc --- /dev/null +++ b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs @@ -0,0 +1,186 @@ +using Content.Server.Chat.Systems; +using Content.Server.Damage.Systems; +using Content.Shared._Fish.JudgeGavel; +using Content.Shared.Chat; +using Content.Shared.DoAfter; +using Content.Shared.Interaction.Events; +using Content.Shared.Mind.Components; +using Content.Shared.Pinpointer; +using Content.Shared.StatusEffect; +using Content.Server.Station.Components; +using Content.Shared.CombatMode.Pacification; +using Content.Shared.Warps; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Physics.Components; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server._Fish.JudgeGavel; + +/// +/// System for the Admin Judge Gavel. +/// +public sealed class JudgeGavelSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly TransformSystem _xformSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GodmodeSystem _godmode = default!; + [Dependency] private readonly PhysicsSystem _physics = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnUseInHand(EntityUid uid, JudgeGavelComponent component, UseInHandEvent args) + { + if (args.Handled) + return; + + // Prevent multiple concurrent swings + if (component.ActiveDoAfter != null) + return; + + StartActivation(uid, component, args.User); + args.Handled = true; + } + + private void StartActivation(EntityUid uid, JudgeGavelComponent component, EntityUid user) + { + // Force speech + var chant = Loc.GetString(component.Chant); + _chat.TrySendInGameICMessage(user, chant, InGameICChatType.Speak, ChatTransmitRange.Normal); + + var ev = new JudgeGavelDoAfterEvent(); + var doAfterArgs = new DoAfterArgs(EntityManager, user, component.DoAfterTime, ev, uid) + { + BreakOnMove = false, + BreakOnDamage = true, + NeedHand = true + }; + + if (_doAfter.TryStartDoAfter(doAfterArgs, out var doAfterId)) + { + component.ActiveDoAfter = doAfterId; + } + } + + private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelDoAfterEvent args) + { + component.ActiveDoAfter = null; + + if (args.Cancelled || args.Handled) + return; + + MapCoordinates? targetMapCoords = null; + var identifier = component.CourtroomBeaconId; + + // 1. Destination Discovery: WarpPoint or Beacon lookup + var warpQuery = EntityQueryEnumerator(); + while (warpQuery.MoveNext(out _, out var warp, out var xform)) + { + if (warp.Location == identifier || (identifier == "station-beacon-courtroom" && warp.Location == "Секторальный суд")) + { + targetMapCoords = _transform.ToMapCoordinates(xform.Coordinates); + break; + } + } + + // Priority 2: NavMapBeacon + if (targetMapCoords == null) + { + var beaconQuery = EntityQueryEnumerator(); + while (beaconQuery.MoveNext(out _, out var beacon, out var xform)) + { + if (beacon.DefaultText == identifier) + { + targetMapCoords = _transform.ToMapCoordinates(xform.Coordinates); + break; + } + } + } + + // 2. Fallback: Grid + Coordinates lookup + if (targetMapCoords == null) + { + EntityUid? centcommGrid = null; + var stationQuery = EntityQueryEnumerator(); + while (stationQuery.MoveNext(out var gridUid, out var becomes)) + { + if (becomes.Id == "centcomm") + { + centcommGrid = gridUid; + break; + } + } + + if (centcommGrid != null) + { + var fallbackCoords = new EntityCoordinates(centcommGrid.Value, new System.Numerics.Vector2(28.5f, 36.5f)); + targetMapCoords = _transform.ToMapCoordinates(fallbackCoords); + } + } + + if (targetMapCoords == null) + return; + + var sourceCoords = _transform.GetMapCoordinates(uid); + + // Play effect at source + Spawn("RadiationPulse", sourceCoords); + + // Deduplicate entities in range to avoid multiple teleports per entity (prevents gibbing/cloning) + var processed = new HashSet(); + var ents = _lookup.GetEntitiesInRange(sourceCoords, component.Range); + + foreach (var mob in ents) + { + if (!processed.Add(mob)) + continue; + + if (!TryComp(mob, out var mind) || !mind.HasMind) + continue; + + + // Apply temporary Godmode to prevent collision deaths during teleport overlapping + /*_godmode.EnableGodmode(mob); + + // Scheduling removal in 2 seconds + Timer.Spawn(TimeSpan.FromSeconds(2), () => + { + if (Exists(mob)) + _godmode.DisableGodmode(mob); + });*/ + + // Apply Pacified (using the exact same signature as GenericStatusEffectEntityEffectSystem does for Pax) + _statusEffects.TryAddStatusEffect(mob, "Pacified", TimeSpan.FromSeconds(component.Duration), true, "Pacified"); + // Spread out targets within 3 tiles to prevent stacking + var offset = _random.NextVector2(3.0f); + var finalTarget = targetMapCoords.Value.Offset(offset); + + // Clear velocity before teleporting to prevent cannonballing into walls/others upon arrival + if (TryComp(mob, out var physics)) + { + _physics.SetLinearVelocity(mob, System.Numerics.Vector2.Zero, body: physics); + _physics.SetAngularVelocity(mob, 0f, body: physics); + } + + // Teleport + _xformSystem.SetMapCoordinates(mob, finalTarget); + + // Effect at individual destination + Spawn("RadiationPulse", finalTarget); + } + + args.Handled = true; + } +} diff --git a/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs b/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs new file mode 100644 index 00000000000..d2094712306 --- /dev/null +++ b/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using Content.Shared.DoAfter; +using Content.Shared.Pinpointer; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared._Fish.JudgeGavel; + +/// +/// Component for the Admin Judge Gavel. +/// When activated, starts a DoAfter that teleports sentient creatures in a radius to the Centcomm courtroom. +/// FIsh edit +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class JudgeGavelComponent : Component +{ + [DataField] + public float Range = 10f; + + [DataField] + public float Duration = 900f; // Seconds of pacifism + + [DataField] + public string CourtroomBeaconId = "station-beacon-courtroom"; + + [DataField] + public float GodmodeDuration = 2f; + + [DataField] + public LocId Chant = "judge-gavel-chant"; + + [DataField] + public float DoAfterTime = 3f; + + /// + /// Tracks the current active DoAfter to prevent multiple concurrent swings. + /// + public DoAfterId? ActiveDoAfter; +} diff --git a/Content.Shared/_Fish/JudgeGavel/JudgeGavelDoAfterEvent.cs b/Content.Shared/_Fish/JudgeGavel/JudgeGavelDoAfterEvent.cs new file mode 100644 index 00000000000..134ac8f003f --- /dev/null +++ b/Content.Shared/_Fish/JudgeGavel/JudgeGavelDoAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._Fish.JudgeGavel; + +[Serializable, NetSerializable] +public sealed partial class JudgeGavelDoAfterEvent : DoAfterEvent +{ + public override DoAfterEvent Clone() => this; +} diff --git a/Resources/Locale/en-US/_fish/judge_gavel.ftl b/Resources/Locale/en-US/_fish/judge_gavel.ftl new file mode 100644 index 00000000000..79b59dec6ac --- /dev/null +++ b/Resources/Locale/en-US/_fish/judge_gavel.ftl @@ -0,0 +1,5 @@ +ent-JudgeGavel = judge gavel + .desc = A special gavel for dispensing justice. + .suffix = Admeme + +judge-gavel-chant = Territorial Expansion: NanoTrasen Sectorial Court diff --git a/Resources/Locale/ru-RU/_fish/judge_gavel.ftl b/Resources/Locale/ru-RU/_fish/judge_gavel.ftl new file mode 100644 index 00000000000..a3f9d29dc14 --- /dev/null +++ b/Resources/Locale/ru-RU/_fish/judge_gavel.ftl @@ -0,0 +1,5 @@ +ent-JudgeGavel = судейский молоток + .desc = Особый молоток для вершения правосудия. + .suffix = Адмеме + +judge-gavel-chant = Расширение территории: Секториальный Суд НаноТрейзен diff --git a/Resources/Prototypes/_Fish/Entities/Objects/Tools/judge_gavel.yml b/Resources/Prototypes/_Fish/Entities/Objects/Tools/judge_gavel.yml new file mode 100644 index 00000000000..5cb2304bc35 --- /dev/null +++ b/Resources/Prototypes/_Fish/Entities/Objects/Tools/judge_gavel.yml @@ -0,0 +1,14 @@ +- type: entity + parent: GavelHammer + id: JudgeGavel + suffix: admeme + components: + - type: JudgeGavel + - type: MeleeWeapon + attackRate: 0.5 + damage: + groups: + Brute: 50 # Admin power + - type: Sprite + sprite: _Sunrise/Objects/Tools/gavelhammer.rsi + state: icon