using Content.Client.CombatMode; using Content.Client.Gameplay; using Content.Client.Hands; using Content.Shared.Mobs.Components; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee.Events; using Content.Shared.StatusEffect; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client.Weapons.Melee; public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem { [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly AnimationPlayerSystem _animation = default!; [Dependency] private readonly InputSystem _inputSystem = default!; private const string MeleeLungeKey = "melee-lunge"; public override void Initialize() { base.Initialize(); InitializeEffect(); _overlayManager.AddOverlay(new MeleeWindupOverlay(EntityManager, _timing, _player, _protoManager)); SubscribeAllEvent(OnDamageEffect); SubscribeNetworkEvent(OnMeleeLunge); } public override void Shutdown() { base.Shutdown(); _overlayManager.RemoveOverlay(); } public override void Update(float frameTime) { base.Update(frameTime); if (!Timing.IsFirstTimePredicted) return; var entityNull = _player.LocalPlayer?.ControlledEntity; if (entityNull == null) return; var entity = entityNull.Value; var weapon = GetWeapon(entity); if (weapon == null) return; if (!CombatMode.IsInCombatMode(entity) || !Blocker.CanAttack(entity)) { weapon.Attacking = false; if (weapon.WindUpStart != null) { EntityManager.RaisePredictiveEvent(new StopHeavyAttackEvent(weapon.Owner)); } return; } // TODO using targeted actions while combat mode is enabled should NOT trigger attacks. var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary); var currentTime = Timing.CurTime; // Heavy attack. if (altDown == BoundKeyState.Down) { // We did the click to end the attack but haven't pulled the key up. if (weapon.Attacking) { return; } // If it's an unarmed attack then do a disarm if (weapon.Owner == entity) { EntityUid? target = null; var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); EntityCoordinates coordinates; if (MapManager.TryFindGridAt(mousePos, out var grid)) { coordinates = EntityCoordinates.FromMap(grid.Owner, mousePos, EntityManager); } else { coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager); } if (_stateManager.CurrentState is GameplayStateBase screen) { target = screen.GetClickedEntity(mousePos); } EntityManager.RaisePredictiveEvent(new DisarmAttackEvent(target, coordinates)); return; } // Otherwise do heavy attack if it's a weapon. // Start a windup if (weapon.WindUpStart == null) { EntityManager.RaisePredictiveEvent(new StartHeavyAttackEvent(weapon.Owner)); weapon.WindUpStart = currentTime; } // Try to do a heavy attack. if (useDown == BoundKeyState.Down) { var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); EntityCoordinates coordinates; // Bro why would I want a ternary here // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression if (MapManager.TryFindGridAt(mousePos, out var grid)) { coordinates = EntityCoordinates.FromMap(grid.Owner, mousePos, EntityManager); } else { coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager); } EntityManager.RaisePredictiveEvent(new HeavyAttackEvent(weapon.Owner, coordinates)); } return; } if (weapon.WindUpStart != null) { EntityManager.RaisePredictiveEvent(new StopHeavyAttackEvent(weapon.Owner)); } // Light attack if (useDown == BoundKeyState.Down) { if (weapon.Attacking || weapon.NextAttack > Timing.CurTime) { return; } var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); var attackerPos = Transform(entity).MapPosition; if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length > weapon.Range) { return; } EntityCoordinates coordinates; // Bro why would I want a ternary here // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression if (MapManager.TryFindGridAt(mousePos, out var grid)) { coordinates = EntityCoordinates.FromMap(grid.Owner, mousePos, EntityManager); } else { coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager); } EntityUid? target = null; // TODO: UI Refactor update I assume if (_stateManager.CurrentState is GameplayStateBase screen) { target = screen.GetClickedEntity(mousePos); } RaisePredictiveEvent(new LightAttackEvent(target, weapon.Owner, coordinates)); return; } if (weapon.Attacking) { RaisePredictiveEvent(new StopAttackEvent(weapon.Owner)); } } protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session) { var xform = Transform(target); var targetCoordinates = xform.Coordinates; var targetLocalAngle = xform.LocalRotation; return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range); } protected override void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform) { // Server never sends the event to us for predictiveeevent. if (_timing.IsFirstTimePredicted) RaiseLocalEvent(new DamageEffectEvent(Color.Red, targets)); } protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) { if (!base.DoDisarm(user, ev, component, session)) return false; if (!TryComp(user, out var combatMode) || combatMode.CanDisarm != true) { return false; } // They need to either have hands... if (!HasComp(ev.Target!.Value)) { // or just be able to be shoved over. if (TryComp(ev.Target!.Value, out var status) && status.AllowedEffects.Contains("KnockedDown")) return true; if (Timing.IsFirstTimePredicted && HasComp(ev.Target.Value)) PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", ev.Target.Value)), ev.Target.Value); return false; } return true; } protected override void Popup(string message, EntityUid? uid, EntityUid? user) { if (!Timing.IsFirstTimePredicted || uid == null) return; PopupSystem.PopupEntity(message, uid.Value); } private void OnMeleeLunge(MeleeLungeEvent ev) { // Entity might not have been sent by PVS. if (Exists(ev.Entity)) DoLunge(ev.Entity, ev.Angle, ev.LocalPos, ev.Animation); } }