Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions Content.Server/_Fish/JudgeGavel/JudgeGavelSystem.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// System for the Admin Judge Gavel.
/// </summary>
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<JudgeGavelComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<JudgeGavelComponent, JudgeGavelDoAfterEvent>(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<WarpPointComponent, TransformComponent>();
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<NavMapBeaconComponent, TransformComponent>();
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<BecomesStationComponent>();
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<EntityUid>();
var ents = _lookup.GetEntitiesInRange(sourceCoords, component.Range);

foreach (var mob in ents)
{
if (!processed.Add(mob))
continue;

if (!TryComp<MindContainerComponent>(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<PhysicsComponent>(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;
}
}
39 changes: 39 additions & 0 deletions Content.Shared/_Fish/JudgeGavel/JudgeGavelComponent.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Component for the Admin Judge Gavel.
/// When activated, starts a DoAfter that teleports sentient creatures in a radius to the Centcomm courtroom.
/// FIsh edit
/// </summary>
[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;

/// <summary>
/// Tracks the current active DoAfter to prevent multiple concurrent swings.
/// </summary>
public DoAfterId? ActiveDoAfter;
}
10 changes: 10 additions & 0 deletions Content.Shared/_Fish/JudgeGavel/JudgeGavelDoAfterEvent.cs
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions Resources/Locale/en-US/_fish/judge_gavel.ftl
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions Resources/Locale/ru-RU/_fish/judge_gavel.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ent-JudgeGavel = судейский молоток
.desc = Особый молоток для вершения правосудия.
.suffix = Адмеме

judge-gavel-chant = Расширение территории: Секториальный Суд НаноТрейзен
Original file line number Diff line number Diff line change
@@ -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
Loading