using System.Linq; using Content.Client.Gameplay; using Content.Shared.CombatMode; using Content.Shared.Effects; using Content.Shared.Hands.Components; using Content.Shared.Mobs.Components; using Content.Shared.StatusEffect; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee.Components; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Components; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.State; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Player; namespace Content.Client.Weapons.Melee; public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem { [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly AnimationPlayerSystem _animation = default!; [Dependency] private readonly InputSystem _inputSystem = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly MapSystem _map = default!; private EntityQuery _xformQuery; private const string MeleeLungeKey = "melee-lunge"; public override void Initialize() { base.Initialize(); _xformQuery = GetEntityQuery(); SubscribeNetworkEvent(OnMeleeLunge); UpdatesOutsidePrediction = true; } public override void FrameUpdate(float frameTime) { base.FrameUpdate(frameTime); UpdateEffects(); } public override void Update(float frameTime) { base.Update(frameTime); if (!Timing.IsFirstTimePredicted) return; var entityNull = _player.LocalEntity; if (entityNull == null) return; var entity = entityNull.Value; if (!TryGetWeapon(entity, out var weaponUid, out var weapon)) return; if (!CombatMode.IsInCombatMode(entity) || !Blocker.CanAttack(entity, weapon: (weaponUid, weapon))) { weapon.Attacking = false; return; } var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary); if (weapon.AutoAttack || useDown != BoundKeyState.Down && altDown != BoundKeyState.Down) { if (weapon.Attacking) { RaisePredictiveEvent(new StopAttackEvent(GetNetEntity(weaponUid))); } } if (weapon.Attacking || weapon.NextAttack > Timing.CurTime) { return; } // TODO using targeted actions while combat mode is enabled should NOT trigger attacks. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); if (mousePos.MapId == MapId.Nullspace) { return; } EntityCoordinates coordinates; if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _)) { coordinates = TransformSystem.ToCoordinates(gridUid, mousePos); } else { coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos); } // If the gun has AltFireComponent, it can be used to attack. if (TryComp(weaponUid, out var gun) && gun.UseKey) { if (!TryComp(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down) return; switch(altFireComponent.AttackType) { case AltFireAttackType.Light: ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon); break; case AltFireAttackType.Heavy: ClientHeavyAttack(entity, coordinates, weaponUid, weapon); break; case AltFireAttackType.Disarm: ClientDisarm(entity, mousePos, coordinates); break; } return; } // Heavy attack. if (altDown == BoundKeyState.Down) { // If it's an unarmed attack then do a disarm if (weapon.AltDisarm && weaponUid == entity) { ClientDisarm(entity, mousePos, coordinates); return; } ClientHeavyAttack(entity, coordinates, weaponUid, weapon); return; } // Light attack if (useDown == BoundKeyState.Down) ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon); } 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, overlapCheck: false); } protected override void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform) { // Server never sends the event to us for predictiveeevent. _color.RaiseEffect(Color.Red, targets, Filter.Local()); } protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { if (!base.DoDisarm(user, ev, meleeUid, component, session)) return false; if (!TryComp(user, out var combatMode) || combatMode.CanDisarm != true) { return false; } var target = GetEntity(ev.Target); // They need to either have hands... if (!HasComp(target!.Value)) { // or just be able to be shoved over. if (TryComp(target, out var status) && status.AllowedEffects.Contains("KnockedDown")) return true; if (Timing.IsFirstTimePredicted && HasComp(target.Value)) PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value); return false; } return true; } /// /// Raises a heavy attack event with the relevant attacked entities. /// This is to avoid lag effecting the client's perspective too much. /// private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component) { // Only run on first prediction to avoid the potential raycast entities changing. if (!_xformQuery.TryGetComponent(user, out var userXform) || !Timing.IsFirstTimePredicted) { return; } var targetMap = TransformSystem.ToMapCoordinates(coordinates); if (targetMap.MapId != userXform.MapID) return; var userPos = TransformSystem.GetWorldPosition(userXform); var direction = targetMap.Position - userPos; var distance = MathF.Min(component.Range, direction.Length()); // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. // Server will validate it with InRangeUnobstructed. var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList()); RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates))); } private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates) { EntityUid? target = null; if (_stateManager.CurrentState is GameplayStateBase screen) target = screen.GetClickedEntity(mousePos); RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates))); } private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent) { var attackerPos = TransformSystem.GetMapCoordinates(attacker); if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length() > meleeComponent.Range) return; EntityUid? target = null; if (_stateManager.CurrentState is GameplayStateBase screen) target = screen.GetClickedEntity(mousePos); // Don't light-attack if interaction will be handling this instead if (Interaction.CombatModeCanHandInteract(attacker, target)) return; RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates))); } private void OnMeleeLunge(MeleeLungeEvent ev) { var ent = GetEntity(ev.Entity); var entWeapon = GetEntity(ev.Weapon); // Entity might not have been sent by PVS. if (Exists(ent) && Exists(entWeapon)) DoLunge(ent, entWeapon, ev.Angle, ev.LocalPos, ev.Animation); } }