From 62dd4fb5fece828986319f76ba96ccdeef75d5f1 Mon Sep 17 00:00:00 2001 From: A-Mironov <65418470+Alexnov33X@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:01:33 +0300 Subject: [PATCH 1/5] WIP --- .../_Fish/JudgeGavel/JudgeGavelSystem.cs | 177 ++++++++++++++++++ .../_Fish/JudgeGavel/JudgeGavelComponent.cs | 33 ++++ .../JudgeGavel/JudgeGavelDoAfterEvent.cs | 10 + Resources/Locale/en-US/_fish/judge_gavel.ftl | 5 + Resources/Locale/ru-RU/_fish/judge_gavel.ftl | 5 + .../Entities/Objects/Tools/judge_gavel.yml | 14 ++ 6 files changed, 244 insertions(+) create mode 100644 Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs create mode 100644 Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs create mode 100644 Content.Shared/_Fish/JudgeGavel/JudgeGavelDoAfterEvent.cs create mode 100644 Resources/Locale/en-US/_fish/judge_gavel.ftl create mode 100644 Resources/Locale/ru-RU/_fish/judge_gavel.ftl create mode 100644 Resources/Prototypes/_Fish/Entities/Objects/Tools/judge_gavel.yml diff --git a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs new file mode 100644 index 00000000000..3d7f47512a8 --- /dev/null +++ b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs @@ -0,0 +1,177 @@ +using Content.Server.Chat.Systems; +using Content.Server.Damage.Systems; +using Content.Server.Station.Systems; +using Content.Shared._Fish.JudgeGavel; +using Content.Shared.Chat; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +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.Damage.Components; +using Content.Shared.Warps; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +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 StationSystem _station = default!; + [Dependency] private readonly TransformSystem _xformSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GodmodeSystem _godmode = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnUseInHand(EntityUid uid, JudgeGavelComponent component, UseInHandEvent args) + { + if (args.Handled) + return; + + StartActivation(uid, component, args.User); + args.Handled = true; + } + + private void OnActivate(EntityUid uid, JudgeGavelComponent component, ActivateInWorldEvent args) + { + if (args.Handled) + 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 + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + MapCoordinates? targetMapCoords = null; + var identifier = component.CourtroomBeaconId; + + // 1. Destination Discovery: WarpPoint or Beacon lookup + // Priority 1: WarpPoint (Abductor method) + var warpQuery = EntityQueryEnumerator(); + while (warpQuery.MoveNext(out _, out var warp, out var xform)) + { + // Match exactly or check for the specific Russian name found in _Fish centcomm.yml + 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 (Hardcoded position for _Fish centcomm courtroom) + 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); + + var ents = _lookup.GetEntitiesInRange(sourceCoords, component.Range); + foreach (var mob in ents) + { + if (!HasComp(mob)) + 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 + _statusEffects.TryAddStatusEffect(mob, "Pacified", TimeSpan.FromSeconds(component.Duration), true); + + // Spread out targets within 3 tiles to prevent stacking + var offset = _random.NextVector2(3.0f); + var finalTarget = targetMapCoords.Value.Offset(offset); + + // 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..9fab9e37eea --- /dev/null +++ b/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs @@ -0,0 +1,33 @@ +using System.Numerics; +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; +} 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..bb6a613bf50 --- /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..a77de88dba4 --- /dev/null +++ b/Resources/Locale/ru-RU/_fish/judge_gavel.ftl @@ -0,0 +1,5 @@ +ent-JudgeGavel = судейский молоток + .desc = Особый молоток для вершения правосудия. + .suffix = admeme + +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 From 6850231293cf7e99a91b5279a4e0430d055f7eba Mon Sep 17 00:00:00 2001 From: A-Mironov <65418470+Alexnov33X@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:35:41 +0300 Subject: [PATCH 2/5] z --- .../_Fish/JudgeGavel/JudgeGavelSystem.cs | 43 ++++++++++++------- .../_Fish/JudgeGavel/JudgeGavelComponent.cs | 6 +++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs index 3d7f47512a8..ac993540a0b 100644 --- a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs +++ b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs @@ -14,6 +14,7 @@ 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; @@ -29,17 +30,16 @@ public sealed class JudgeGavelSystem : EntitySystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; - [Dependency] private readonly StationSystem _station = 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(OnActivate); SubscribeLocalEvent(OnDoAfter); } @@ -48,13 +48,8 @@ private void OnUseInHand(EntityUid uid, JudgeGavelComponent component, UseInHand if (args.Handled) return; - StartActivation(uid, component, args.User); - args.Handled = true; - } - - private void OnActivate(EntityUid uid, JudgeGavelComponent component, ActivateInWorldEvent args) - { - if (args.Handled) + // Prevent multiple concurrent swings + if (component.ActiveDoAfter != null) return; StartActivation(uid, component, args.User); @@ -75,11 +70,16 @@ private void StartActivation(EntityUid uid, JudgeGavelComponent component, Entit NeedHand = true }; - _doAfter.TryStartDoAfter(doAfterArgs); + 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; @@ -87,11 +87,9 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD var identifier = component.CourtroomBeaconId; // 1. Destination Discovery: WarpPoint or Beacon lookup - // Priority 1: WarpPoint (Abductor method) var warpQuery = EntityQueryEnumerator(); while (warpQuery.MoveNext(out _, out var warp, out var xform)) { - // Match exactly or check for the specific Russian name found in _Fish centcomm.yml if (warp.Location == identifier || (identifier == "station-beacon-courtroom" && warp.Location == "Секторальный суд")) { targetMapCoords = _transform.ToMapCoordinates(xform.Coordinates); @@ -113,7 +111,7 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD } } - // 2. Fallback: Grid + Coordinates lookup (Hardcoded position for _Fish centcomm courtroom) + // 2. Fallback: Grid + Coordinates lookup if (targetMapCoords == null) { EntityUid? centcommGrid = null; @@ -138,19 +136,25 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD 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 (!HasComp(mob)) continue; // Apply temporary Godmode to prevent collision deaths during teleport overlapping _godmode.EnableGodmode(mob); - + // Scheduling removal in 2 seconds Timer.Spawn(TimeSpan.FromSeconds(2), () => { @@ -165,6 +169,13 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD 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); diff --git a/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs b/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs index 9fab9e37eea..d2094712306 100644 --- a/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs +++ b/Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Shared.DoAfter; using Content.Shared.Pinpointer; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -30,4 +31,9 @@ public sealed partial class JudgeGavelComponent : Component [DataField] public float DoAfterTime = 3f; + + /// + /// Tracks the current active DoAfter to prevent multiple concurrent swings. + /// + public DoAfterId? ActiveDoAfter; } From fe3bdd7765db972f3696f955566ec57ae22698b3 Mon Sep 17 00:00:00 2001 From: A-Mironov <65418470+Alexnov33X@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:32:16 +0300 Subject: [PATCH 3/5] Works now --- Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs index ac993540a0b..976cbcc0bef 100644 --- a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs +++ b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs @@ -149,9 +149,10 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD if (!processed.Add(mob)) continue; - if (!HasComp(mob)) + if (!TryComp(mob, out var mind) || !mind.HasMind) continue; + // Apply temporary Godmode to prevent collision deaths during teleport overlapping _godmode.EnableGodmode(mob); From 599f540bf2c819b454894f08a98e51c98f8a2d0d Mon Sep 17 00:00:00 2001 From: A-Mironov <65418470+Alexnov33X@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:47:25 +0300 Subject: [PATCH 4/5] =?UTF-8?q?Godmode=20=D0=BC=D0=B5=D1=88=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=86=D0=B8=D1=84=D0=B8=D0=B7=D0=BC=D1=83,=20?= =?UTF-8?q?=D0=B6=D0=B0=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs index 976cbcc0bef..5d011ccbddc 100644 --- a/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs +++ b/Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs @@ -1,16 +1,14 @@ using Content.Server.Chat.Systems; using Content.Server.Damage.Systems; -using Content.Server.Station.Systems; using Content.Shared._Fish.JudgeGavel; using Content.Shared.Chat; using Content.Shared.DoAfter; -using Content.Shared.Interaction; 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.Damage.Components; +using Content.Shared.CombatMode.Pacification; using Content.Shared.Warps; using Robust.Server.GameObjects; using Robust.Shared.Map; @@ -154,18 +152,17 @@ private void OnDoAfter(EntityUid uid, JudgeGavelComponent component, JudgeGavelD // Apply temporary Godmode to prevent collision deaths during teleport overlapping - _godmode.EnableGodmode(mob); + /*_godmode.EnableGodmode(mob); // Scheduling removal in 2 seconds Timer.Spawn(TimeSpan.FromSeconds(2), () => { if (Exists(mob)) _godmode.DisableGodmode(mob); - }); - - // Apply Pacified - _statusEffects.TryAddStatusEffect(mob, "Pacified", TimeSpan.FromSeconds(component.Duration), true); + });*/ + // 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); From 1b86e621f1665f2089490b26cf05a3990ef0159c Mon Sep 17 00:00:00 2001 From: A-Mironov <65418470+Alexnov33X@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:48:44 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=B0=D0=B4=D0=BC=D0=B5=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/Locale/en-US/_fish/judge_gavel.ftl | 2 +- Resources/Locale/ru-RU/_fish/judge_gavel.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Locale/en-US/_fish/judge_gavel.ftl b/Resources/Locale/en-US/_fish/judge_gavel.ftl index bb6a613bf50..79b59dec6ac 100644 --- a/Resources/Locale/en-US/_fish/judge_gavel.ftl +++ b/Resources/Locale/en-US/_fish/judge_gavel.ftl @@ -1,5 +1,5 @@ ent-JudgeGavel = judge gavel .desc = A special gavel for dispensing justice. - .suffix = admeme + .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 index a77de88dba4..a3f9d29dc14 100644 --- a/Resources/Locale/ru-RU/_fish/judge_gavel.ftl +++ b/Resources/Locale/ru-RU/_fish/judge_gavel.ftl @@ -1,5 +1,5 @@ ent-JudgeGavel = судейский молоток .desc = Особый молоток для вершения правосудия. - .suffix = admeme + .suffix = Адмеме judge-gavel-chant = Расширение территории: Секториальный Суд НаноТрейзен