Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Numerics;
using Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.Enums;
using Robust.Shared.Maths;

namespace Content.Pirate.Client._JustDecor.Weapons.SmartRevolver;

/// <summary>
/// Overlay that shows a target indicator around the selected entity for the smart revolver.
/// </summary>
public sealed class SmartRevolverOverlay : Overlay
{
private readonly IEntityManager _entityManager;
private readonly IPlayerManager _playerManager;
private readonly IEyeManager _eyeManager;
private readonly SharedHandsSystem _hands;

public override OverlaySpace Space => OverlaySpace.ScreenSpace;

public SmartRevolverOverlay(IEntityManager entityManager, IPlayerManager playerManager, IEyeManager eyeManager)
{
_entityManager = entityManager;
_playerManager = playerManager;
_eyeManager = eyeManager;
_hands = _entityManager.System<SharedHandsSystem>();
}

protected override void Draw(in OverlayDrawArgs args)
{
var player = _playerManager.LocalSession?.AttachedEntity;
if (player == null)
return;

if (!_hands.TryGetActiveItem(player.Value, out var activeHandEntity))
return;

if (!_entityManager.TryGetComponent<SmartRevolverComponent>(activeHandEntity, out var comp))
return;

if (comp.SelectedTarget == null || !_entityManager.EntityExists(comp.SelectedTarget.Value))
return;

if (!_entityManager.TryGetComponent<TransformComponent>(comp.SelectedTarget.Value, out var targetXform))
return;

if (targetXform.MapID != args.MapId)
return;

var screenPos = _eyeManager.CoordinatesToScreen(targetXform.Coordinates);
var handle = args.ScreenHandle;
var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f;

// Прицільний індикатор
var color = Color.Gold;
var boxSize = new Vector2(60f, 60f) * uiScale;
var halfSize = boxSize / 2;
var topLeft = screenPos.Position - halfSize;
var borderThickness = 2.5f * uiScale;

// Зовнішня рамка
handle.DrawRect(UIBox2.FromDimensions(topLeft, new Vector2(boxSize.X, borderThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(0, boxSize.Y - borderThickness), new Vector2(boxSize.X, borderThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft, new Vector2(borderThickness, boxSize.Y)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - borderThickness, 0), new Vector2(borderThickness, boxSize.Y)), color);

// Кути для "lock-on" ефекту
var cornerLength = 12f * uiScale;
var cornerThickness = 3f * uiScale;
var cornerOffset = 5f * uiScale;

// Верхній лівий кут
handle.DrawRect(UIBox2.FromDimensions(topLeft - new Vector2(cornerOffset, cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft - new Vector2(cornerOffset, cornerOffset), new Vector2(cornerThickness, cornerLength)), color);

// Верхній правий кут
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerLength + cornerOffset, -cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerThickness + cornerOffset, -cornerOffset), new Vector2(cornerThickness, cornerLength)), color);

// Нижній лівий кут
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(-cornerOffset, boxSize.Y - cornerThickness + cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(-cornerOffset, boxSize.Y - cornerLength + cornerOffset), new Vector2(cornerThickness, cornerLength)), color);

// Нижній правий кут
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerLength + cornerOffset, boxSize.Y - cornerThickness + cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerThickness + cornerOffset, boxSize.Y - cornerLength + cornerOffset), new Vector2(cornerThickness, cornerLength)), color);

// Центральна частинка
var diamondSize = 5f * uiScale;
handle.DrawRect(UIBox2.FromDimensions(screenPos.Position - new Vector2(diamondSize / 2, diamondSize / 2), new Vector2(diamondSize, diamondSize)), color);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.Input;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Content.Shared.CombatMode;
using Robust.Shared.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.IoC;
using Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
using Content.Shared.Mobs.Components;
using Content.Shared.Damage;
using Robust.Shared.Player;
using Content.Shared.Hands.EntitySystems;
using Content.Client.ContextMenu.UI;
using Content.Client.UserInterface.Systems.Actions;
using Content.Client.UserInterface.Systems.Hands;
using Content.Client.UserInterface.Systems.Inventory;
using Content.Client.UserInterface.Systems.Storage;

namespace Content.Pirate.Client._JustDecor.Weapons.SmartRevolver;

public sealed class SmartRevolverSystem : EntitySystem
{
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;

private SmartRevolverOverlay _overlayInst = default!;

public override void Initialize()
{
base.Initialize();

_overlayInst = new SmartRevolverOverlay(_entity, _player, _eye);
_overlay.AddOverlay(_overlayInst);
CommandBinds.Builder
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnRightClick),
typeof(EntityMenuUIController),
typeof(HandsUIController),
typeof(InventoryUIController),
typeof(StorageUIController),
typeof(ActionUIController))
.Register<SmartRevolverSystem>();
}

private bool OnRightClick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
return TryHandleInstantTargeting(session, coords, uid);
}

private bool TryHandleInstantTargeting(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (session == null)
return false;

var player = session?.AttachedEntity;
if (player == null)
return false;

// Instant targeting only in combat mode
if (!_combat.IsInCombatMode(player.Value))
return false;

// Must be holding Smart Revolver
if (!_hands.TryGetActiveItem(player.Value, out var activeItem))
return false;

if (!TryComp(activeItem.Value, out SmartRevolverComponent? revolver))
return false;

EntityUid target = uid;

if (!target.IsValid() || target == player || target == activeItem.Value)
{
RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
return true;
}

var playerPos = _transform.GetMapCoordinates(player.Value).Position;
var targetPos = target.IsValid()
? _transform.GetMapCoordinates(target).Position
: _transform.ToMapCoordinates(coords).Position;

if ((targetPos - playerPos).Length() > revolver.MaxTargetDistance)
{
RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
return true;
}

if (HasComp<MobStateComponent>(target) || HasComp<DamageableComponent>(target))
{
RaiseNetworkEvent(new SmartRevolverSetTargetMessage(GetNetEntity(target)));
return true;
}

// Клік в пусте місце -> очищення цілі
RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
return true;
}
Comment on lines +57 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Зайвий оператор ?. та мертва гілка тернарного оператора.

  1. Рядок 62: session вже перевірено на null у рядку 59, тому session?.AttachedEntity може бути спрощено до session.AttachedEntity.
  2. Рядок 86-88: target.IsValid() тернарний оператор завжди буде true в цій точці, оскільки невалідні цілі вже оброблені на рядках 79-83. Гілка false (_transform.ToMapCoordinates(coords).Position) — мертвий код.
♻️ Запропоноване спрощення
-        var player = session?.AttachedEntity;
+        var player = session.AttachedEntity;
-        var targetPos = target.IsValid()
-            ? _transform.GetMapCoordinates(target).Position
-            : _transform.ToMapCoordinates(coords).Position;
+        var targetPos = _transform.GetMapCoordinates(target).Position;
🤖 Prompt for AI Agents
In
`@Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs`
around lines 57 - 105, The method TryHandleInstantTargeting contains redundant
null-propagation and a dead ternary branch: replace session?.AttachedEntity with
session.AttachedEntity (since session is already null-checked) and simplify the
targetPos assignment to always use _transform.GetMapCoordinates(target).Position
(remove the _transform.ToMapCoordinates(coords).Position branch and the
unnecessary target.IsValid() check there), ensuring you still use
target.IsValid() earlier to guard this code path; update references in
TryHandleInstantTargeting accordingly.


public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay(_overlayInst);
CommandBinds.Unregister<SmartRevolverSystem>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Numerics;
using Robust.Shared.GameStates;

namespace Content.Pirate.Shared._JustDecor.Weapons.Ranged;

/// <summary>
/// Component for projectiles that can ricochet off walls to hit a target.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RicochetProjectileComponent : Component
{
/// <summary>
/// The target entity that the projectile should try to hit via ricochets.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Target;

/// <summary>
/// Maximum number of bounces allowed before the projectile stops ricocheting.
/// </summary>
[DataField, AutoNetworkedField]
public int MaxBounces = 4;

/// <summary>
/// Current number of bounces that have occurred.
/// </summary>
[DataField, AutoNetworkedField]
public int CurrentBounces = 0;

/// <summary>
/// Planned waypoints for the ricochet path (wall hit positions).
/// </summary>
[DataField]
public List<Vector2> PlannedPath = new();

/// <summary>
/// Whether the projectile should follow the planned path.
/// If false, the projectile will calculate ricochets dynamically on each bounce.
/// </summary>
[DataField, AutoNetworkedField]
public bool FollowPlannedPath = true;

/// <summary>
/// Minimum speed required for ricochet to occur. Below this speed, projectile will stop.
/// </summary>
[DataField]
public float MinimumRicochetSpeed = 5f;

/// <summary>
/// Speed multiplier applied after each bounce (energy loss).
/// </summary>
[DataField]
public float SpeedRetentionOnBounce = 0.95f;

/// <summary>
/// How strongly the projectile steers towards its target every frame.
/// 0.0 means no steering, 1.0 means instant snap.
/// </summary>
[DataField]
public float SteeringStrength = 1.0f; // High default for 100% hits

/// <summary>
/// Delay before homing starts (seconds).
/// </summary>
[DataField]
public float HomingDelay = 0.2f;

/// <summary>
/// Current accumulator for homing delay.
/// </summary>
[ViewVariables]
public float HomingAccumulator = 0f;

/// <summary>
/// How much speed to add per bounce.
/// </summary>
[DataField]
public float SpeedBonusPerBounce = 5f;

/// <summary>
/// Lifetime bonus applied after each bounce.
/// </summary>
[DataField]
public float BounceLifetimeBonus = 3f;

/// <summary>
/// Target number of bounces before hitting the final target.
/// </summary>
[DataField]
public int TargetBounces = 0;
}
Loading
Loading