diff --git a/Content.Client/Administration/Systems/AdminSystem.Menu.cs b/Content.Client/Administration/Systems/AdminSystem.Menu.cs index 93b6770141..ded24fcd44 100644 --- a/Content.Client/Administration/Systems/AdminSystem.Menu.cs +++ b/Content.Client/Administration/Systems/AdminSystem.Menu.cs @@ -155,7 +155,7 @@ namespace Content.Client.Administration.Systems if (function == EngineKeyFunctions.UIClick) _clientConsoleHost.ExecuteCommand($"vv {uid}"); - else if (function == ContentKeyFunctions.OpenContextMenu) + else if (function == EngineKeyFunctions.AltUse) _verbSystem.VerbMenu.OpenVerbMenu(uid, true); else return; @@ -173,7 +173,7 @@ namespace Content.Client.Administration.Systems if (function == EngineKeyFunctions.UIClick) _clientConsoleHost.ExecuteCommand($"vv {uid}"); - else if (function == ContentKeyFunctions.OpenContextMenu) + else if (function == EngineKeyFunctions.AltUse) _verbSystem.VerbMenu.OpenVerbMenu(uid, true); else return; diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index b760ced4c4..e21646e61f 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -54,7 +54,7 @@ namespace Content.Client.Administration.UI.CustomControls if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label) label.Text = GetText(selectedPlayer); } - else if (args.Event.Function == ContentKeyFunctions.OpenContextMenu) + else if (args.Event.Function == EngineKeyFunctions.AltUse) { _verbSystem.VerbMenu.OpenVerbMenu(selectedPlayer.EntityUid); } diff --git a/Content.Client/CombatMode/CombatModeSystem.cs b/Content.Client/CombatMode/CombatModeSystem.cs index 96a3d7da40..174f3406df 100644 --- a/Content.Client/CombatMode/CombatModeSystem.cs +++ b/Content.Client/CombatMode/CombatModeSystem.cs @@ -4,8 +4,8 @@ using Content.Shared.Targeting; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Shared.GameStates; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; namespace Content.Client.CombatMode { @@ -23,6 +23,16 @@ namespace Content.Client.CombatMode SubscribeLocalEvent((_, component, _) => component.PlayerAttached()); SubscribeLocalEvent((_, component, _) => component.PlayerDetached()); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(EntityUid uid, SharedCombatModeComponent component, ref ComponentHandleState args) + { + if (args.Current is not CombatModeComponentState state) + return; + + component.IsInCombatMode = state.IsInCombatMode; + component.ActiveZone = state.TargetingZone; } public override void Shutdown() @@ -33,8 +43,12 @@ namespace Content.Client.CombatMode public bool IsInCombatMode() { - return EntityManager.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out CombatModeComponent? combatMode) && - combatMode.IsInCombatMode; + var entity = _playerManager.LocalPlayer?.ControlledEntity; + + if (entity == null) + return false; + + return IsInCombatMode(entity.Value); } private void OnTargetingZoneChanged(TargetingZone obj) @@ -42,8 +56,4 @@ namespace Content.Client.CombatMode EntityManager.RaisePredictiveEvent(new CombatModeSystemMessages.SetTargetZoneMessage(obj)); } } - - public static class A - { - } } diff --git a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs index 1719ac6429..1b9c13014b 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuPresenter.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; +using Content.Client.CombatMode; using Content.Client.Examine; using Content.Client.Gameplay; using Content.Client.Verbs; using Content.Client.Viewport; using Content.Shared.CCVar; +using Content.Shared.CombatMode; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -45,6 +47,7 @@ namespace Content.Client.ContextMenu.UI private readonly VerbSystem _verbSystem; private readonly ExamineSystem _examineSystem; + private readonly SharedCombatModeSystem _combatMode; /// /// This maps the currently displayed entities to the actual GUI elements. @@ -59,12 +62,13 @@ namespace Content.Client.ContextMenu.UI IoCManager.InjectDependencies(this); _verbSystem = verbSystem; - _examineSystem = EntitySystem.Get(); + _examineSystem = _entityManager.EntitySysManager.GetEntitySystem(); + _combatMode = _entityManager.EntitySysManager.GetEntitySystem(); _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true); CommandBinds.Builder - .Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true)) + .Bind(EngineKeyFunctions.AltUse, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true)) .Register(); } @@ -109,7 +113,7 @@ namespace Content.Client.ContextMenu.UI return; // open verb menu? - if (args.Function == ContentKeyFunctions.OpenContextMenu) + if (args.Function == EngineKeyFunctions.AltUse) { _verbSystem.VerbMenu.OpenVerbMenu(entity.Value); args.Handle(); @@ -160,6 +164,9 @@ namespace Content.Client.ContextMenu.UI if (_stateManager.CurrentState is not GameplayStateBase) return false; + if (_combatMode.IsInCombatMode(args.Session?.AttachedEntity)) + return false; + var coords = args.Coordinates.ToMap(_entityManager); if (_verbSystem.TryGetEntityMenuEntities(coords, out var entities)) diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 524681ffb6..7eb2f9d995 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -50,10 +50,6 @@ public sealed class DoAfterOverlay : Overlay } var worldPosition = _transform.GetWorldPosition(xform); - - if (!args.WorldAABB.Contains(worldPosition)) - continue; - var index = 0; var worldMatrix = Matrix3.CreateTranslation(worldPosition); diff --git a/Content.Client/Effects/EffectVisualsComponent.cs b/Content.Client/Effects/EffectVisualsComponent.cs index 05a1231f28..30662126a2 100644 --- a/Content.Client/Effects/EffectVisualsComponent.cs +++ b/Content.Client/Effects/EffectVisualsComponent.cs @@ -1,8 +1,7 @@ namespace Content.Client.Effects; +/// +/// Deletes the attached entity whenever any animation completes. Used for temporary client-side entities. +/// [RegisterComponent] -public sealed class EffectVisualsComponent : Component -{ - public float Length; - public float Accumulator = 0f; -} +public sealed class EffectVisualsComponent : Component {} diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index fe46cf4c26..abee8f9177 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -27,7 +27,6 @@ namespace Content.Client.Input common.AddFunction(ContentKeyFunctions.TakeScreenshot); common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI); common.AddFunction(ContentKeyFunctions.Point); - common.AddFunction(ContentKeyFunctions.OpenContextMenu); // Not in engine, because engine cannot check for sanbox/admin status before starting placement. common.AddFunction(ContentKeyFunctions.EditorCopyObject); diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index 9b2750adae..78d6842584 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -11,6 +11,7 @@ using Content.Shared.Interaction; using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Shared.GameObjects; +using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -70,7 +71,7 @@ namespace Content.Client.Items.Managers _entitySystemManager.GetEntitySystem() .DoExamine(item.Value); } - else if (args.Function == ContentKeyFunctions.OpenContextMenu) + else if (args.Function == EngineKeyFunctions.AltUse) { _entitySystemManager.GetEntitySystem().VerbMenu.OpenVerbMenu(item.Value); } diff --git a/Content.Client/Items/Systems/ItemSystem.cs b/Content.Client/Items/Systems/ItemSystem.cs index 2277712235..cfe7bd6fb9 100644 --- a/Content.Client/Items/Systems/ItemSystem.cs +++ b/Content.Client/Items/Systems/ItemSystem.cs @@ -12,7 +12,6 @@ namespace Content.Client.Items.Systems; public sealed class ItemSystem : SharedItemSystem { - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly IResourceCache _resCache = default!; public override void Initialize() @@ -30,8 +29,8 @@ public sealed class ItemSystem : SharedItemSystem public override void VisualsChanged(EntityUid uid) { // if the item is in a container, it might be equipped to hands or inventory slots --> update visuals. - if (_containerSystem.TryGetContainingContainer(uid, out var container)) - RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID), true); + if (Container.TryGetContainingContainer(uid, out var container)) + RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID)); } /// diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index e3a7ed37f9..912fe28395 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -105,6 +105,7 @@ namespace Content.Client.Options.UI.Tabs AddHeader("ui-options-header-interaction-basic"); AddButton(EngineKeyFunctions.Use); + AddButton(EngineKeyFunctions.AltUse); AddButton(ContentKeyFunctions.UseItemInHand); AddButton(ContentKeyFunctions.AltUseItemInHand); AddButton(ContentKeyFunctions.ActivateItemInWorld); @@ -134,7 +135,6 @@ namespace Content.Client.Options.UI.Tabs AddButton(ContentKeyFunctions.CycleChatChannelForward); AddButton(ContentKeyFunctions.CycleChatChannelBackward); AddButton(ContentKeyFunctions.OpenCharacterMenu); - AddButton(ContentKeyFunctions.OpenContextMenu); AddButton(ContentKeyFunctions.OpenCraftingMenu); AddButton(ContentKeyFunctions.OpenInventoryMenu); AddButton(ContentKeyFunctions.OpenInfo); diff --git a/Content.Client/Rotation/RotationVisualizer.cs b/Content.Client/Rotation/RotationVisualizer.cs index 0e188aa1e6..6bddf2e10f 100644 --- a/Content.Client/Rotation/RotationVisualizer.cs +++ b/Content.Client/Rotation/RotationVisualizer.cs @@ -37,6 +37,11 @@ namespace Content.Client.Rotation var entMan = IoCManager.Resolve(); var sprite = entMan.GetComponent(component.Owner); + if (sprite.Rotation.Equals(rotation)) + { + return; + } + if (!entMan.TryGetComponent(sprite.Owner, out AnimationPlayerComponent? animation)) { sprite.Rotation = rotation; diff --git a/Content.Client/Weapons/Melee/Components/MeleeLungeComponent.cs b/Content.Client/Weapons/Melee/Components/MeleeLungeComponent.cs deleted file mode 100644 index f400324de7..0000000000 --- a/Content.Client/Weapons/Melee/Components/MeleeLungeComponent.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; - -namespace Content.Client.Weapons.Melee.Components -{ - [RegisterComponent] - public sealed class MeleeLungeComponent : Component - { - private const float ResetTime = 0.3f; - private const float BaseOffset = 0.25f; - - private Angle _angle; - private float _time; - - public void SetData(Angle angle) - { - _angle = angle; - _time = 0; - } - - public void Update(float frameTime) - { - _time += frameTime; - - var offset = Vector2.Zero; - var deleteSelf = false; - - if (_time > ResetTime) - { - deleteSelf = true; - } - else - { - offset = _angle.RotateVec((0, -BaseOffset)); - offset *= (ResetTime - _time) / ResetTime; - } - - var entMan = IoCManager.Resolve(); - - if (entMan.TryGetComponent(Owner, out ISpriteComponent? spriteComponent)) - { - spriteComponent.Offset = offset; - } - - if (deleteSelf) - { - entMan.RemoveComponent(Owner); - } - } - } -} diff --git a/Content.Client/Weapons/Melee/Components/MeleeWeaponArcAnimationComponent.cs b/Content.Client/Weapons/Melee/Components/MeleeWeaponArcAnimationComponent.cs deleted file mode 100644 index 6d0ceaea0e..0000000000 --- a/Content.Client/Weapons/Melee/Components/MeleeWeaponArcAnimationComponent.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Content.Shared.Weapons.Melee; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; - -namespace Content.Client.Weapons.Melee.Components -{ - [RegisterComponent] - public sealed class MeleeWeaponArcAnimationComponent : Component - { - [Dependency] private readonly IEntityManager _entMan = default!; - private MeleeWeaponAnimationPrototype? _meleeWeaponAnimation; - - private float _timer; - private SpriteComponent? _sprite; - private Angle _baseAngle; - - protected override void Initialize() - { - base.Initialize(); - - _sprite = _entMan.GetComponent(Owner); - } - - public void SetData(MeleeWeaponAnimationPrototype prototype, Angle baseAngle, EntityUid attacker, bool followAttacker = true) - { - _meleeWeaponAnimation = prototype; - _sprite?.AddLayer(new RSI.StateId(prototype.State)); - _baseAngle = baseAngle; - if(followAttacker) - _entMan.GetComponent(Owner).AttachParent(attacker); - } - - internal void Update(float frameTime) - { - if (_meleeWeaponAnimation == null) - { - return; - } - - _timer += frameTime; - - var (r, g, b, a) = - Vector4.Clamp(_meleeWeaponAnimation.Color + _meleeWeaponAnimation.ColorDelta * _timer, Vector4.Zero, Vector4.One); - - if (_sprite != null) - { - _sprite.Color = new Color(r, g, b, a); - } - - var transform = _entMan.GetComponent(Owner); - - switch (_meleeWeaponAnimation.ArcType) - { - case WeaponArcType.Slash: - var angle = Angle.FromDegrees(_meleeWeaponAnimation.Width)/2; - transform.WorldRotation = _baseAngle + Angle.Lerp(-angle, angle, (float) (_timer / _meleeWeaponAnimation.Length.TotalSeconds)); - break; - - case WeaponArcType.Poke: - transform.WorldRotation = _baseAngle; - - if (_sprite != null) - { - _sprite.Offset -= (0, _meleeWeaponAnimation.Speed * frameTime); - } - break; - } - - - if (_meleeWeaponAnimation.Length.TotalSeconds <= _timer) - { - _entMan.DeleteEntity(Owner); - } - } - } -} diff --git a/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs b/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs new file mode 100644 index 0000000000..0c8d6bb9ba --- /dev/null +++ b/Content.Client/Weapons/Melee/Components/WeaponArcVisualsComponent.cs @@ -0,0 +1,18 @@ +namespace Content.Client.Weapons.Melee.Components; + +/// +/// Used for melee attack animations. Typically just has a fadeout. +/// +[RegisterComponent] +public sealed class WeaponArcVisualsComponent : Component +{ + [ViewVariables, DataField("animation")] + public WeaponArcAnimation Animation = WeaponArcAnimation.None; +} + +public enum WeaponArcAnimation : byte +{ + None, + Thrust, + Slash, +} diff --git a/Content.Client/Weapons/Melee/MeleeArcOverlay.cs b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs new file mode 100644 index 0000000000..02bc433601 --- /dev/null +++ b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs @@ -0,0 +1,75 @@ +using Content.Shared.CombatMode; +using Content.Shared.Weapons.Melee; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map; + +namespace Content.Client.Weapons.Melee; + +/// +/// Debug overlay showing the arc and range of a melee weapon. +/// +public sealed class MeleeArcOverlay : Overlay +{ + private readonly IEntityManager _entManager; + private readonly IEyeManager _eyeManager; + private readonly IInputManager _inputManager; + private readonly IPlayerManager _playerManager; + private readonly MeleeWeaponSystem _melee; + private readonly SharedCombatModeSystem _combatMode; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + public MeleeArcOverlay(IEntityManager entManager, IEyeManager eyeManager, IInputManager inputManager, IPlayerManager playerManager, MeleeWeaponSystem melee, SharedCombatModeSystem combatMode) + { + _entManager = entManager; + _eyeManager = eyeManager; + _inputManager = inputManager; + _playerManager = playerManager; + _melee = melee; + _combatMode = combatMode; + } + + protected override void Draw(in OverlayDrawArgs args) + { + var player = _playerManager.LocalPlayer?.ControlledEntity; + + if (!_entManager.TryGetComponent(player, out var xform) || + !_combatMode.IsInCombatMode(player)) + { + return; + } + + var weapon = _melee.GetWeapon(player.Value); + + if (weapon == null) + return; + + var mousePos = _inputManager.MouseScreenPosition; + var mapPos = _eyeManager.ScreenToMap(mousePos); + + if (mapPos.MapId != args.MapId) + return; + + var playerPos = xform.MapPosition; + + if (mapPos.MapId != playerPos.MapId) + return; + + var diff = mapPos.Position - playerPos.Position; + + if (diff.Equals(Vector2.Zero)) + return; + + diff = diff.Normalized * Math.Min(weapon.Range, diff.Length); + args.WorldHandle.DrawLine(playerPos.Position, playerPos.Position + diff, Color.Aqua); + + if (weapon.Angle.Theta == 0) + return; + + args.WorldHandle.DrawLine(playerPos.Position, playerPos.Position + new Angle(-weapon.Angle / 2).RotateVec(diff), Color.Orange); + args.WorldHandle.DrawLine(playerPos.Position, playerPos.Position + new Angle(weapon.Angle / 2).RotateVec(diff), Color.Orange); + } +} diff --git a/Content.Client/Weapons/Melee/MeleeLungeSystem.cs b/Content.Client/Weapons/Melee/MeleeLungeSystem.cs deleted file mode 100644 index ed3aa0137a..0000000000 --- a/Content.Client/Weapons/Melee/MeleeLungeSystem.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Client.Weapons.Melee.Components; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; - -namespace Content.Client.Weapons.Melee -{ - [UsedImplicitly] - public sealed class MeleeLungeSystem : EntitySystem - { - public override void FrameUpdate(float frameTime) - { - base.FrameUpdate(frameTime); - - foreach (var meleeLungeComponent in EntityManager.EntityQuery(true)) - { - meleeLungeComponent.Update(frameTime); - } - } - } -} diff --git a/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs b/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs new file mode 100644 index 0000000000..6b259a7fd5 --- /dev/null +++ b/Content.Client/Weapons/Melee/MeleeSpreadCommand.cs @@ -0,0 +1,40 @@ +using Content.Shared.CombatMode; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Console; +using Robust.Shared.Map; + +namespace Content.Client.Weapons.Melee; + + +public sealed class MeleeSpreadCommand : IConsoleCommand +{ + public string Command => "showmeleespread"; + public string Description => "Shows the current weapon's range and arc for debugging"; + public string Help => $"{Command}"; + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var collection = IoCManager.Instance; + + if (collection == null) + return; + + var overlayManager = collection.Resolve(); + + if (overlayManager.RemoveOverlay()) + { + return; + } + + var sysManager = collection.Resolve(); + + overlayManager.AddOverlay(new MeleeArcOverlay( + collection.Resolve(), + collection.Resolve(), + collection.Resolve(), + collection.Resolve(), + sysManager.GetEntitySystem(), + sysManager.GetEntitySystem())); + } +} diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs index 50e46c9b36..e6223598ea 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs @@ -2,31 +2,17 @@ using Content.Shared.Weapons; using Content.Shared.Weapons.Melee; using Robust.Client.Animations; using Robust.Client.GameObjects; +using Robust.Shared.Animations; using Robust.Shared.Utility; namespace Content.Client.Weapons.Melee; public sealed partial class MeleeWeaponSystem { - private static readonly Animation DefaultDamageAnimation = new() - { - Length = TimeSpan.FromSeconds(DamageAnimationLength), - AnimationTracks = - { - new AnimationTrackComponentProperty() - { - ComponentType = typeof(SpriteComponent), - Property = nameof(SpriteComponent.Color), - KeyFrames = - { - new AnimationTrackProperty.KeyFrame(Color.Red, 0f), - new AnimationTrackProperty.KeyFrame(Color.White, DamageAnimationLength) - } - } - } - }; - - private const float DamageAnimationLength = 0.15f; + /// + /// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register. + /// + private const float DamageAnimationLength = 0.30f; private const string DamageAnimationKey = "damage-effect"; private void InitializeEffect() @@ -47,27 +33,25 @@ public sealed partial class MeleeWeaponSystem /// /// Gets the red effect animation whenever the server confirms something is hit /// - public Animation? GetDamageAnimation(EntityUid uid, SpriteComponent? sprite = null) + private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null) { if (!Resolve(uid, ref sprite, false)) return null; // 90% of them are going to be this so why allocate a new class. - if (sprite.Color.Equals(Color.White)) - return DefaultDamageAnimation; - return new Animation { Length = TimeSpan.FromSeconds(DamageAnimationLength), AnimationTracks = { - new AnimationTrackComponentProperty() + new AnimationTrackComponentProperty { ComponentType = typeof(SpriteComponent), Property = nameof(SpriteComponent.Color), + InterpolationMode = AnimationInterpolationMode.Linear, KeyFrames = { - new AnimationTrackProperty.KeyFrame(Color.Red * sprite.Color, 0f), + new AnimationTrackProperty.KeyFrame(color * sprite.Color, 0f), new AnimationTrackProperty.KeyFrame(sprite.Color, DamageAnimationLength) } } @@ -77,36 +61,44 @@ public sealed partial class MeleeWeaponSystem private void OnDamageEffect(DamageEffectEvent ev) { - if (Deleted(ev.Entity)) - return; + var color = ev.Color; - var player = EnsureComp(ev.Entity); - - // Need to stop the existing animation first to ensure the sprite color is fixed. - // Otherwise we might lerp to a red colour instead. - if (_animation.HasRunningAnimation(ev.Entity, player, DamageAnimationKey)) + foreach (var ent in ev.Entities) { - _animation.Stop(ev.Entity, player, DamageAnimationKey); + if (Deleted(ent)) + { + continue; + } + + var player = EnsureComp(ent); + player.NetSyncEnabled = false; + + // Need to stop the existing animation first to ensure the sprite color is fixed. + // Otherwise we might lerp to a red colour instead. + if (_animation.HasRunningAnimation(ent, player, DamageAnimationKey)) + { + _animation.Stop(ent, player, DamageAnimationKey); + } + + if (!TryComp(ent, out var sprite)) + { + continue; + } + + if (TryComp(ent, out var effect)) + { + sprite.Color = effect.Color; + } + + var animation = GetDamageAnimation(ent, color, sprite); + + if (animation == null) + continue; + + var comp = EnsureComp(ent); + comp.NetSyncEnabled = false; + comp.Color = sprite.Color; + _animation.Play(player, animation, DamageAnimationKey); } - - if (!TryComp(ev.Entity, out var sprite)) - { - return; - } - - if (TryComp(ev.Entity, out var effect)) - { - sprite.Color = effect.Color; - } - - var animation = GetDamageAnimation(ev.Entity, sprite); - - if (animation == null) - return; - - var comp = EnsureComp(ev.Entity); - comp.NetSyncEnabled = false; - comp.Color = sprite.Color; - _animation.Play(player, DefaultDamageAnimation, DamageAnimationKey); } } diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index f69d62580e..c34aea2e5b 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -1,133 +1,416 @@ -using System; +using Content.Client.CombatMode; +using Content.Client.Gameplay; +using Content.Client.Hands; using Content.Client.Weapons.Melee.Components; -using Content.Shared.Examine; using Content.Shared.Weapons.Melee; -using JetBrains.Annotations; +using Content.Shared.Weapons.Melee.Events; +using Robust.Client.Animations; using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Maths; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Client.State; +using Robust.Shared.Animations; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using static Content.Shared.Weapons.Melee.MeleeWeaponSystemMessages; -namespace Content.Client.Weapons.Melee +namespace Content.Client.Weapons.Melee; + +public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem { - public sealed partial class MeleeWeaponSystem : EntitySystem + [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 IResourceCache _cache = 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() { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly AnimationPlayerSystem _animation = default!; - [Dependency] private readonly EffectSystem _effectSystem = default!; + base.Initialize(); + InitializeEffect(); + _overlayManager.AddOverlay(new MeleeWindupOverlay(EntityManager, _timing, _protoManager, _cache)); + SubscribeNetworkEvent(OnDamageEffect); + SubscribeNetworkEvent(OnMeleeLunge); + } - public override void Initialize() + 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)) { - InitializeEffect(); - SubscribeNetworkEvent(PlayWeaponArc); - SubscribeNetworkEvent(PlayLunge); - SubscribeNetworkEvent(OnDamageEffect); - } - - public override void FrameUpdate(float frameTime) - { - base.FrameUpdate(frameTime); - - foreach (var arcAnimationComponent in EntityManager.EntityQuery(true)) + weapon.Attacking = false; + if (weapon.WindUpStart != null) { - arcAnimationComponent.Update(frameTime); + EntityManager.RaisePredictiveEvent(new StopHeavyAttackEvent(weapon.Owner)); } + + return; } - private void PlayWeaponArc(PlayMeleeWeaponAnimationMessage msg) + var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); + var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.AltUse); + var currentTime = Timing.CurTime; + + // Heavy attack. + if (altDown == BoundKeyState.Down) { - if (!_prototypeManager.TryIndex(msg.ArcPrototype, out MeleeWeaponAnimationPrototype? weaponArc)) + // We did the click to end the attack but haven't pulled the key up. + if (weapon.Attacking) { - Logger.Error("Tried to play unknown weapon arc prototype '{0}'", msg.ArcPrototype); return; } - var attacker = msg.Attacker; - if (!EntityManager.EntityExists(msg.Attacker)) + // If it's an unarmed attack then do a disarm + if (weapon.Owner == entity) { - // FIXME: This should never happen. - Logger.Error($"Tried to play a weapon arc {msg.ArcPrototype}, but the attacker does not exist. attacker={msg.Attacker}, source={msg.Source}"); + EntityUid? target = null; + + var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + EntityCoordinates coordinates; + + if (MapManager.TryFindGridAt(mousePos, out var grid)) + { + coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mousePos, EntityManager); + } + else + { + coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager); + } + + if (_stateManager.CurrentState is GameplayStateBase screen) + { + target = screen.GetEntityUnderPosition(mousePos); + } + + EntityManager.RaisePredictiveEvent(new DisarmAttackEvent(target, coordinates)); return; } - if (!Deleted(attacker)) + // Otherwise do heavy attack if it's a weapon. + + // Start a windup + if (weapon.WindUpStart == null) { - var lunge = attacker.EnsureComponent(); - lunge.SetData(msg.Angle); - - var entity = EntityManager.SpawnEntity(weaponArc.Prototype, EntityManager.GetComponent(attacker).Coordinates); - EntityManager.GetComponent(entity).LocalRotation = msg.Angle; - - var weaponArcAnimation = EntityManager.GetComponent(entity); - weaponArcAnimation.SetData(weaponArc, msg.Angle, attacker, msg.ArcFollowAttacker); - - // Due to ISpriteComponent limitations, weapons that don't use an RSI won't have this effect. - if (EntityManager.EntityExists(msg.Source) && - msg.TextureEffect && - EntityManager.TryGetComponent(msg.Source, out ISpriteComponent? sourceSprite) && - sourceSprite.BaseRSI?.Path is { } path) - { - var curTime = _gameTiming.CurTime; - var effect = new EffectSystemMessage - { - EffectSprite = path.ToString(), - RsiState = sourceSprite.LayerGetState(0).Name, - Coordinates = EntityManager.GetComponent(attacker).Coordinates, - Color = Vector4.Multiply(new Vector4(255, 255, 255, 125), 1.0f), - ColorDelta = Vector4.Multiply(new Vector4(0, 0, 0, -10), 1.0f), - Velocity = msg.Angle.ToWorldVec(), - Acceleration = msg.Angle.ToWorldVec() * 5f, - Born = curTime, - DeathTime = curTime.Add(TimeSpan.FromMilliseconds(300f)), - }; - - _effectSystem.CreateEffect(effect); - } + EntityManager.RaisePredictiveEvent(new StartHeavyAttackEvent(weapon.Owner)); + weapon.WindUpStart = currentTime; } - foreach (var hit in msg.Hits) + // Try to do a heavy attack. + if (useDown == BoundKeyState.Down) { - if (!EntityManager.EntityExists(hit)) + 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)) { - continue; + coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mousePos, EntityManager); + } + else + { + coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager); } - if (!EntityManager.TryGetComponent(hit, out ISpriteComponent? sprite)) - { - continue; - } - - var originalColor = sprite.Color; - var newColor = Color.Red * originalColor; - sprite.Color = newColor; - - hit.SpawnTimer(100, () => - { - // Only reset back to the original color if something else didn't change the color in the mean time. - if (sprite.Color == newColor) - { - sprite.Color = originalColor; - } - }); + EntityManager.RaisePredictiveEvent(new HeavyAttackEvent(weapon.Owner, coordinates)); } + + return; } - private void PlayLunge(PlayLungeAnimationMessage msg) + if (weapon.WindUpStart != null) { - if (EntityManager.EntityExists(msg.Source)) + EntityManager.RaisePredictiveEvent(new StopHeavyAttackEvent(weapon.Owner)); + } + + // Light attack + if (useDown == BoundKeyState.Down) + { + if (weapon.Attacking || weapon.NextAttack > Timing.CurTime) { - msg.Source.EnsureComponent().SetData(msg.Angle); + return; + } + + 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.GridEntityId, mousePos, EntityManager); } else { - // FIXME: This should never happen. - Logger.Error($"Tried to play a lunge animation, but the entity \"{msg.Source}\" does not exist."); + 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.GetEntityUnderPosition(mousePos); + } + + EntityManager.RaisePredictiveEvent(new LightAttackEvent(target, weapon.Owner, coordinates)); + + return; + } + + if (weapon.Attacking) + { + EntityManager.RaisePredictiveEvent(new StopAttackEvent(weapon.Owner)); + } + } + + protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component) + { + if (!base.DoDisarm(user, ev, component)) + return false; + + if (!HasComp(user)) + return false; + + // If target doesn't have hands then we can't disarm so will let the player know it's pointless. + if (!HasComp(ev.Target!.Value)) + { + if (Timing.IsFirstTimePredicted) + PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", ev.Target.Value)), ev.Target.Value, Filter.Local()); + + 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, Filter.Local()); + } + + private void OnMeleeLunge(MeleeLungeEvent ev) + { + DoLunge(ev.Entity, ev.Angle, ev.LocalPos, ev.Animation); + } + + /// + /// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation. + /// + public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation) + { + if (!Timing.IsFirstTimePredicted) + return; + + var lunge = GetLungeAnimation(localPos); + + // Stop any existing lunges on the user. + _animation.Stop(user, MeleeLungeKey); + _animation.Play(user, lunge, MeleeLungeKey); + + // Clientside entity to spawn + if (animation != null) + { + var animationUid = Spawn(animation, new EntityCoordinates(user, Vector2.Zero)); + + if (localPos != Vector2.Zero && TryComp(animationUid, out var sprite)) + { + sprite[0].AutoAnimated = false; + + if (TryComp(animationUid, out var arcComponent)) + { + sprite.NoRotation = true; + sprite.Rotation = localPos.ToWorldAngle(); + var distance = Math.Clamp(localPos.Length / 2f, 0.2f, 1f); + + switch (arcComponent.Animation) + { + case WeaponArcAnimation.Slash: + _animation.Play(animationUid, GetSlashAnimation(sprite, angle), "melee-slash"); + break; + case WeaponArcAnimation.Thrust: + _animation.Play(animationUid, GetThrustAnimation(sprite, distance), "melee-thrust"); + break; + case WeaponArcAnimation.None: + sprite.Offset = localPos.Normalized * distance; + _animation.Play(animationUid, GetStaticAnimation(sprite), "melee-fade"); + break; + } + } } } } + + private Animation GetSlashAnimation(SpriteComponent sprite, Angle arc) + { + var slashStart = 0.03f; + var slashEnd = 0.065f; + var length = slashEnd + 0.05f; + var startRotation = sprite.Rotation - arc / 2; + var endRotation = sprite.Rotation + arc / 2; + sprite.NoRotation = true; + + return new Animation() + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(startRotation, 0f), + new AnimationTrackProperty.KeyFrame(startRotation, slashStart), + new AnimationTrackProperty.KeyFrame(endRotation, slashEnd) + } + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(startRotation.RotateVec(new Vector2(0f, -1f)), 0f), + new AnimationTrackProperty.KeyFrame(startRotation.RotateVec(new Vector2(0f, -1f)), slashStart), + new AnimationTrackProperty.KeyFrame(endRotation.RotateVec(new Vector2(0f, -1f)), slashEnd) + } + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Color), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Color, slashEnd), + new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), length), + } + } + } + }; + } + + private Animation GetThrustAnimation(SpriteComponent sprite, float distance) + { + var length = 0.15f; + var thrustEnd = 0.05f; + + return new Animation() + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Rotation.RotateVec(new Vector2(0f, -distance / 5f)), 0f), + new AnimationTrackProperty.KeyFrame(sprite.Rotation.RotateVec(new Vector2(0f, -distance)), thrustEnd), + new AnimationTrackProperty.KeyFrame(sprite.Rotation.RotateVec(new Vector2(0f, -distance)), length), + } + }, + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Color), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Color, thrustEnd), + new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), length), + } + } + } + }; + } + + /// + /// Get the fadeout for static weapon arcs. + /// + private Animation GetStaticAnimation(SpriteComponent sprite) + { + var length = 0.15f; + + return new() + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Color), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(sprite.Color, 0f), + new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), length) + } + } + } + }; + } + + /// + /// Get the sprite offset animation to use for mob lunges. + /// + private Animation GetLungeAnimation(Vector2 direction) + { + var length = 0.1f; + + return new Animation + { + Length = TimeSpan.FromSeconds(length), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(direction.Normalized * 0.15f, 0f), + new AnimationTrackProperty.KeyFrame(Vector2.Zero, length) + } + } + } + }; + } } diff --git a/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs b/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs new file mode 100644 index 0000000000..7506efa29a --- /dev/null +++ b/Content.Client/Weapons/Melee/MeleeWindupOverlay.cs @@ -0,0 +1,135 @@ +using Content.Client.DoAfter; +using Content.Client.Resources; +using Content.Shared.Weapons.Melee; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Weapons.Melee; + +public sealed class MeleeWindupOverlay : Overlay +{ + private readonly IEntityManager _entManager; + private readonly IGameTiming _timing; + private readonly SharedTransformSystem _transform; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + private readonly Texture _texture; + private readonly ShaderInstance _shader; + + public MeleeWindupOverlay(IEntityManager entManager, IGameTiming timing, IPrototypeManager protoManager, IResourceCache cache) + { + _entManager = entManager; + _timing = timing; + _transform = _entManager.EntitySysManager.GetEntitySystem(); + _texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"); + _shader = protoManager.Index("unshaded").Instance(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var handle = args.WorldHandle; + var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero; + var spriteQuery = _entManager.GetEntityQuery(); + var xformQuery = _entManager.GetEntityQuery(); + var tickTime = (float) _timing.TickPeriod.TotalSeconds; + var tickFraction = _timing.TickFraction / (float) ushort.MaxValue * tickTime; + + // If you use the display UI scale then need to set max(1f, displayscale) because 0 is valid. + const float scale = 1f; + var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale)); + var rotationMatrix = Matrix3.CreateRotation(-rotation); + handle.UseShader(_shader); + var currentTime = _timing.CurTime; + + foreach (var comp in _entManager.EntityQuery(true)) + { + if (comp.WindUpStart == null || + comp.Attacking) + { + continue; + } + + if (!xformQuery.TryGetComponent(comp.Owner, out var xform) || + xform.MapID != args.MapId) + { + continue; + } + + var worldPosition = _transform.GetWorldPosition(xform); + var worldMatrix = Matrix3.CreateTranslation(worldPosition); + Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld); + Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty); + + handle.SetTransform(matty); + var offset = -_texture.Height / scale; + + // Use the sprite itself if we know its bounds. This means short or tall sprites don't get overlapped + // by the bar. + float yOffset; + if (spriteQuery.TryGetComponent(comp.Owner, out var sprite)) + { + yOffset = -sprite.Bounds.Height / 2f - 0.05f; + } + else + { + yOffset = -0.5f; + } + + // Position above the entity (we've already applied the matrix transform to the entity itself) + // Offset by the texture size for every do_after we have. + var position = new Vector2(-_texture.Width / 2f / EyeManager.PixelsPerMeter, + yOffset / scale + offset / EyeManager.PixelsPerMeter * scale); + + // Draw the underlying bar texture + handle.DrawTexture(_texture, position); + + // Draw the items overlapping the texture + const float startX = 2f; + const float endX = 22f; + + // Area marking where to release + var ReleaseWidth = 2f * SharedMeleeWeaponSystem.GracePeriod / (float) comp.WindupTime.TotalSeconds * EyeManager.PixelsPerMeter; + var releaseMiddle = (endX - startX) / 2f + startX; + + var releaseBox = new Box2(new Vector2(releaseMiddle - ReleaseWidth / 2f, 3f) / EyeManager.PixelsPerMeter, + new Vector2(releaseMiddle + ReleaseWidth / 2f, 4f) / EyeManager.PixelsPerMeter); + + releaseBox = releaseBox.Translated(position); + handle.DrawRect(releaseBox, Color.LimeGreen); + + // Wraps around back to 0 + var totalDuration = comp.WindupTime.TotalSeconds * 2; + + var elapsed = (currentTime - comp.WindUpStart.Value).TotalSeconds % (2 * totalDuration); + var value = elapsed / totalDuration; + + if (value > 1) + { + value = 2 - value; + } + + var fraction = (float) value; + + var xPos = (endX - startX) * fraction + startX; + + // In pixels + const float Width = 2f; + // If we hit the end we won't draw half the box so we need to subtract the end pos from it + var endPos = xPos + Width / 2f; + + var box = new Box2(new Vector2(Math.Max(startX, endPos - Width), 3f) / EyeManager.PixelsPerMeter, + new Vector2(Math.Min(endX, endPos), 4f) / EyeManager.PixelsPerMeter); + + box = box.Translated(position); + handle.DrawRect(box, Color.White); + } + + handle.UseShader(null); + handle.SetTransform(Matrix3.Identity); + } +} diff --git a/Content.Client/Weapons/Ranged/Commands/ShowSpreadCommand.cs b/Content.Client/Weapons/Ranged/Commands/ShowSpreadCommand.cs index 704bd3d642..861d7d75c6 100644 --- a/Content.Client/Weapons/Ranged/Commands/ShowSpreadCommand.cs +++ b/Content.Client/Weapons/Ranged/Commands/ShowSpreadCommand.cs @@ -5,7 +5,7 @@ namespace Content.Client.Weapons.Ranged; public sealed class ShowSpreadCommand : IConsoleCommand { - public string Command => "showspread"; + public string Command => "showgunspread"; public string Description => $"Shows gun spread overlay for debugging"; public string Help => $"{Command}"; public void Execute(IConsoleShell shell, string argStr, string[] args) diff --git a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs index d1d4d72447..b817bffee4 100644 --- a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs +++ b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs @@ -13,11 +13,11 @@ public sealed class GunSpreadOverlay : Overlay public override OverlaySpace Space => OverlaySpace.WorldSpace; private IEntityManager _entManager; - private IEyeManager _eye; - private IGameTiming _timing; - private IInputManager _input; - private IPlayerManager _player; - private GunSystem _guns; + private readonly IEyeManager _eye; + private readonly IGameTiming _timing; + private readonly IInputManager _input; + private readonly IPlayerManager _player; + private readonly GunSystem _guns; public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGameTiming timing, IInputManager input, IPlayerManager player, GunSystem system) { diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index 29de7ba73e..64296ac34b 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -6,11 +6,9 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Item; -using Content.Shared.Weapons.Melee; using NUnit.Framework; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Reflection; @@ -78,18 +76,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); - var attack = false; var interactUsing = false; var interactHand = false; await server.WaitAssertion(() => { - testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target)); attack = true; }; testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; - interactionSystem.DoAttack(user, sEntities.GetComponent(target).Coordinates, false, target); interactionSystem.UserInteraction(user, sEntities.GetComponent(target).Coordinates, target); - Assert.That(attack); Assert.That(interactUsing, Is.False); Assert.That(interactHand); @@ -144,18 +138,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); - var attack = false; var interactUsing = false; var interactHand = false; await server.WaitAssertion(() => { - testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target)); attack = true; }; testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; - interactionSystem.DoAttack(user, sEntities.GetComponent(target).Coordinates, false, target); interactionSystem.UserInteraction(user, sEntities.GetComponent(target).Coordinates, target); - Assert.That(attack, Is.False); Assert.That(interactUsing, Is.False); Assert.That(interactHand, Is.False); @@ -208,18 +198,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); - var attack = false; var interactUsing = false; var interactHand = false; await server.WaitAssertion(() => { - testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target)); attack = true; }; testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; - interactionSystem.DoAttack(user, sEntities.GetComponent(target).Coordinates, false, target); interactionSystem.UserInteraction(user, sEntities.GetComponent(target).Coordinates, target); - Assert.That(attack); Assert.That(interactUsing, Is.False); Assert.That(interactHand); @@ -273,18 +259,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(entitySystemManager.TryGetEntitySystem(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem(out var testInteractionSystem)); - var attack = false; var interactUsing = false; var interactHand = false; await server.WaitAssertion(() => { - testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(target)); attack = true; }; testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; - interactionSystem.DoAttack(user, sEntities.GetComponent(target).Coordinates, false, target); interactionSystem.UserInteraction(user, sEntities.GetComponent(target).Coordinates, target); - Assert.That(attack, Is.False); Assert.That(interactUsing, Is.False); Assert.That(interactHand, Is.False); @@ -344,7 +326,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click await server.WaitIdleAsync(); - var attack = false; var interactUsing = false; var interactHand = false; await server.WaitAssertion(() => @@ -352,19 +333,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click Assert.That(container.Insert(user)); Assert.That(sEntities.GetComponent(user).Parent.Owner, Is.EqualTo(containerEntity)); - testInteractionSystem.AttackEvent = (_, _, ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); attack = true; }; testInteractionSystem.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactUsing = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactHand = true; }; - interactionSystem.DoAttack(user, sEntities.GetComponent(target).Coordinates, false, target); interactionSystem.UserInteraction(user, sEntities.GetComponent(target).Coordinates, target); - Assert.That(attack, Is.False); Assert.That(interactUsing, Is.False); Assert.That(interactHand, Is.False); - interactionSystem.DoAttack(user, sEntities.GetComponent(containerEntity).Coordinates, false, containerEntity); interactionSystem.UserInteraction(user, sEntities.GetComponent(containerEntity).Coordinates, containerEntity); - Assert.That(attack); Assert.That(interactUsing, Is.False); Assert.That(interactHand); @@ -383,14 +359,12 @@ namespace Content.IntegrationTests.Tests.Interaction.Click [Reflect(false)] public sealed class TestInteractionSystem : EntitySystem { - public ComponentEventHandler? AttackEvent; public EntityEventHandler? InteractUsingEvent; public EntityEventHandler? InteractHandEvent; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((u, c, e) => AttackEvent?.Invoke(u, c, e)); SubscribeLocalEvent((e) => InteractUsingEvent?.Invoke(e)); SubscribeLocalEvent((e) => InteractHandEvent?.Invoke(e)); } diff --git a/Content.Server/Abilities/Boxer/BoxingSystem.cs b/Content.Server/Abilities/Boxer/BoxingSystem.cs index 44f5ebe64e..e6672e466e 100644 --- a/Content.Server/Abilities/Boxer/BoxingSystem.cs +++ b/Content.Server/Abilities/Boxer/BoxingSystem.cs @@ -1,13 +1,6 @@ -using Content.Server.Weapon.Melee; -using Content.Server.Stunnable; -using Content.Shared.Inventory.Events; -using Content.Server.Weapon.Melee.Components; -using Content.Server.Clothing.Components; -using Content.Server.Damage.Components; using Content.Server.Damage.Events; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Random; +using Content.Server.Weapons.Melee.Events; +using Content.Shared.Weapons.Melee; using Robust.Shared.Containers; namespace Content.Server.Abilities.Boxer @@ -24,25 +17,22 @@ namespace Content.Server.Abilities.Boxer SubscribeLocalEvent(OnStamHit); } - private void OnInit(EntityUid uid, BoxerComponent boxer, ComponentInit args) + private void OnInit(EntityUid uid, BoxerComponent component, ComponentInit args) { if (TryComp(uid, out var meleeComp)) - meleeComp.Range *= boxer.RangeBonus; + meleeComp.Range *= component.RangeBonus; } private void GetDamageModifiers(EntityUid uid, BoxerComponent component, ItemMeleeDamageEvent args) { - if (component.UnarmedModifiers == default!) - { - Logger.Warning("BoxerComponent on " + uid + " couldn't get damage modifiers. Know that adding components with damage modifiers through VV or similar is unsupported."); - return; - } - args.ModifiersList.Add(component.UnarmedModifiers); } + private void OnStamHit(EntityUid uid, BoxingGlovesComponent component, StaminaMeleeHitEvent args) { - _containerSystem.TryGetContainingContainer(uid, out var equipee); - if (TryComp(equipee?.Owner, out var boxer)) + if (!_containerSystem.TryGetContainingContainer(uid, out var equipee)) + return; + + if (TryComp(equipee.Owner, out var boxer)) args.Multiplier *= boxer.BoxingGlovesModifier; } } diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 0fe5cb695b..7ebfe542d9 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -3,7 +3,7 @@ using Content.Server.Atmos.Components; using Content.Server.Stunnable; using Content.Server.Temperature.Components; using Content.Server.Temperature.Systems; -using Content.Server.Weapon.Melee; +using Content.Server.Weapons.Melee.Events; using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.Atmos; diff --git a/Content.Server/Chemistry/Components/HyposprayComponent.cs b/Content.Server/Chemistry/Components/HyposprayComponent.cs index 85b0c356d1..92d8bb4a99 100644 --- a/Content.Server/Chemistry/Components/HyposprayComponent.cs +++ b/Content.Server/Chemistry/Components/HyposprayComponent.cs @@ -1,7 +1,6 @@ using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; using Content.Server.Interaction.Components; -using Content.Server.Weapon.Melee; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; @@ -12,6 +11,7 @@ using Robust.Shared.Audio; using Robust.Shared.Player; using System.Diagnostics.CodeAnalysis; using Content.Server.Interaction; +using Content.Server.Weapons.Melee; namespace Content.Server.Chemistry.Components { @@ -78,7 +78,8 @@ namespace Content.Server.Chemistry.Components target.Value.PopupMessage(Loc.GetString("hypospray-component-feel-prick-message")); var meleeSys = EntitySystem.Get(); var angle = Angle.FromWorldVec(_entMan.GetComponent(target.Value).WorldPosition - _entMan.GetComponent(user).WorldPosition); - meleeSys.SendLunge(angle, user); + // TODO: This should just be using melee attacks... + // meleeSys.SendLunge(angle, user); } SoundSystem.Play(_injectSound.GetSound(), Filter.Pvs(user), user); diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs index e7363ee5db..7bcc39a59a 100644 --- a/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs +++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs @@ -1,7 +1,10 @@ +using System.Linq; using Content.Server.Chemistry.Components; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; namespace Content.Server.Chemistry.EntitySystems { @@ -10,7 +13,7 @@ namespace Content.Server.Chemistry.EntitySystems private void InitializeHypospray() { SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnClickAttack); + SubscribeLocalEvent(OnAttack); SubscribeLocalEvent(OnSolutionChange); SubscribeLocalEvent(OnUseInHand); } @@ -39,12 +42,12 @@ namespace Content.Server.Chemistry.EntitySystems comp.TryDoInject(target, user); } - public void OnClickAttack(EntityUid uid, HyposprayComponent comp, ClickAttackEvent args) + public void OnAttack(EntityUid uid, HyposprayComponent comp, MeleeHitEvent args) { - if (args.Target == null) + if (!args.HitEntities.Any()) return; - comp.TryDoInject(args.Target.Value, args.User); + comp.TryDoInject(args.HitEntities.First(), args.User); } } } diff --git a/Content.Server/CombatMode/CombatModeSystem.cs b/Content.Server/CombatMode/CombatModeSystem.cs index 418116d2c4..b539f55254 100644 --- a/Content.Server/CombatMode/CombatModeSystem.cs +++ b/Content.Server/CombatMode/CombatModeSystem.cs @@ -1,132 +1,22 @@ -using Content.Server.Actions.Events; -using Content.Server.Administration.Components; -using Content.Server.Administration.Logs; -using Content.Server.CombatMode.Disarm; -using Content.Server.Hands.Components; -using Content.Server.Popups; -using Content.Server.Contests; -using Content.Server.Weapon.Melee; -using Content.Shared.ActionBlocker; -using Content.Shared.Audio; using Content.Shared.CombatMode; -using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.IdentityManagement; -using Content.Shared.Stunnable; using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Random; -using Robust.Shared.Physics; +using Robust.Shared.GameStates; namespace Content.Server.CombatMode { [UsedImplicitly] public sealed class CombatModeSystem : SharedCombatModeSystem { - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly MeleeWeaponSystem _meleeWeaponSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger= default!; - [Dependency] private readonly IRobustRandom _random = default!; - - [Dependency] private readonly ContestsSystem _contests = default!; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnEntityActionPerform); + SubscribeLocalEvent(OnGetState); } - private void OnEntityActionPerform(EntityUid uid, SharedCombatModeComponent component, DisarmActionEvent args) + private void OnGetState(EntityUid uid, SharedCombatModeComponent component, ref ComponentGetState args) { - if (args.Handled) - return; - - if (!_actionBlockerSystem.CanAttack(args.Performer)) - return; - - if (TryComp(args.Performer, out var hands) - && hands.ActiveHand != null - && !hands.ActiveHand.IsEmpty) - { - _popupSystem.PopupEntity(Loc.GetString("disarm-action-free-hand"), args.Performer, Filter.Entities(args.Performer)); - return; - } - - EntityUid? inTargetHand = null; - - if (TryComp(args.Target, out HandsComponent? targetHandsComponent) - && targetHandsComponent.ActiveHand != null - && !targetHandsComponent.ActiveHand.IsEmpty) - { - inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value; - } - - var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer,inTargetHand); - - if (inTargetHand != null) - { - RaiseLocalEvent(inTargetHand.Value, attemptEvent, true); - } - RaiseLocalEvent(args.Target, attemptEvent, true); - if (attemptEvent.Cancelled) - return; - - var diff = Transform(args.Target).MapPosition.Position - Transform(args.Performer).MapPosition.Position; - var angle = Angle.FromWorldVec(diff); - - var filterAll = Filter.Pvs(args.Performer); - var filterOther = filterAll.RemoveWhereAttachedEntity(e => e == args.Performer); - - args.Handled = true; - var chance = CalculateDisarmChance(args.Performer, args.Target, inTargetHand, component); - if (_random.Prob(chance)) - { - SoundSystem.Play(component.DisarmFailSound.GetSound(), Filter.Pvs(args.Performer), args.Performer, AudioHelpers.WithVariation(0.025f)); - - var msgOther = Loc.GetString( - "disarm-action-popup-message-other-clients", - ("performerName", Identity.Entity(args.Performer, EntityManager)), - ("targetName", Identity.Entity(args.Target, EntityManager))); - - var msgUser = Loc.GetString("disarm-action-popup-message-cursor", ("targetName", Identity.Entity(args.Target, EntityManager))); - - _popupSystem.PopupEntity(msgOther, args.Performer, filterOther); - _popupSystem.PopupEntity(msgUser, args.Performer, Filter.Entities(args.Performer)); - - _meleeWeaponSystem.SendLunge(angle, args.Performer); - return; - } - - _meleeWeaponSystem.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target }); - SoundSystem.Play(component.DisarmSuccessSound.GetSound(), filterAll, args.Performer, AudioHelpers.WithVariation(0.025f)); - _adminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(args.Performer):user} used disarm on {ToPrettyString(args.Target):target}"); - - var eventArgs = new DisarmedEvent() { Target = args.Target, Source = args.Performer, PushProbability = (1 - chance) }; - RaiseLocalEvent(args.Target, eventArgs, true); - } - - - private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, SharedCombatModeComponent disarmerComp) - { - if (HasComp(disarmer)) - return 1.0f; - - if (HasComp(disarmed)) - return 0.0f; - - var contestResults = 1 - _contests.OverallStrengthContest(disarmer, disarmed); - - float chance = (disarmerComp.BaseDisarmFailChance + contestResults); - - if (inTargetHand != null && TryComp(inTargetHand, out var malus)) - { - chance += malus.Malus; - } - - return Math.Clamp(chance, 0f, 1f); + args.State = new CombatModeComponentState(component.IsInCombatMode, component.ActiveZone); } } } diff --git a/Content.Server/Contests/ContestsSystem.cs b/Content.Server/Contests/ContestsSystem.cs index 5850d68d02..4eaa524df0 100644 --- a/Content.Server/Contests/ContestsSystem.cs +++ b/Content.Server/Contests/ContestsSystem.cs @@ -14,7 +14,7 @@ namespace Content.Server.Contests /// >1 = Advantage to roller /// <1 = Advantage to target /// Roller should be the entity with an advantage from being bigger/healthier/more skilled, etc. - /// + /// public sealed class ContestsSystem : EntitySystem { [Dependency] private readonly SharedMobStateSystem _mobStateSystem = default!; @@ -27,13 +27,10 @@ namespace Content.Server.Contests if (!Resolve(roller, ref rollerPhysics, false) || !Resolve(target, ref targetPhysics, false)) return 1f; - if (rollerPhysics == null || targetPhysics == null) - return 1f; - if (targetPhysics.FixturesMass == 0) return 1f; - return (rollerPhysics.FixturesMass / targetPhysics.FixturesMass); + return rollerPhysics.FixturesMass / targetPhysics.FixturesMass; } /// @@ -47,18 +44,15 @@ namespace Content.Server.Contests if (!Resolve(roller, ref rollerDamage, false) || !Resolve(target, ref targetDamage, false)) return 1f; - if (rollerDamage == null || targetDamage == null) - return 1f; - // First, we'll see what health they go into crit at. float rollerThreshold = 100f; float targetThreshold = 100f; - if (TryComp(roller, out var rollerState) && rollerState != null && + if (TryComp(roller, out var rollerState) && _mobStateSystem.TryGetEarliestIncapacitatedState(rollerState, 10000, out _, out var rollerCritThreshold)) rollerThreshold = (float) rollerCritThreshold; - if (TryComp(target, out var targetState) && targetState != null && + if (TryComp(target, out var targetState) && _mobStateSystem.TryGetEarliestIncapacitatedState(targetState, 10000, out _, out var targetCritThreshold)) targetThreshold = (float) targetCritThreshold; @@ -97,8 +91,9 @@ namespace Content.Server.Contests var massMultiplier = massWeight / weightTotal; var stamMultiplier = stamWeight / weightTotal; - return ((DamageContest(roller, target) * damageMultiplier) + (MassContest(roller, target) * massMultiplier) - + (StaminaContest(roller, target) * stamMultiplier)); + return DamageContest(roller, target) * damageMultiplier + + MassContest(roller, target) * massMultiplier + + StaminaContest(roller, target) * stamMultiplier; } /// @@ -109,6 +104,7 @@ namespace Content.Server.Contests { return score switch { + // TODO: Should just be a curve <= 0 => 1f, <= 0.25f => 0.9f, <= 0.5f => 0.75f, diff --git a/Content.Server/Damage/Components/StaminaDamageOnHitComponent.cs b/Content.Server/Damage/Components/StaminaDamageOnHitComponent.cs index 84d2d8eb51..88e92c6175 100644 --- a/Content.Server/Damage/Components/StaminaDamageOnHitComponent.cs +++ b/Content.Server/Damage/Components/StaminaDamageOnHitComponent.cs @@ -7,10 +7,4 @@ public sealed class StaminaDamageOnHitComponent : Component { [ViewVariables(VVAccess.ReadWrite), DataField("damage")] public float Damage = 30f; - - /// - /// Play a sound when this knocks down an entity. - /// - [DataField("knockdownSound")] - public SoundSpecifier? KnockdownSound; } diff --git a/Content.Server/Damage/Systems/StaminaSystem.cs b/Content.Server/Damage/Systems/StaminaSystem.cs index 4c34746bde..e1e4d26b79 100644 --- a/Content.Server/Damage/Systems/StaminaSystem.cs +++ b/Content.Server/Damage/Systems/StaminaSystem.cs @@ -1,9 +1,9 @@ using Content.Server.Damage.Components; using Content.Server.Damage.Events; using Content.Server.Popups; -using Content.Server.Weapon.Melee; using Content.Server.Administration.Logs; using Content.Server.CombatMode; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Alert; using Content.Shared.Rounding; using Content.Shared.Stunnable; @@ -123,7 +123,7 @@ public sealed class StaminaSystem : EntitySystem foreach (var comp in toHit) { var oldDamage = comp.StaminaDamage; - TakeStaminaDamage(comp.Owner, damage / toHit.Count, comp, component.KnockdownSound); + TakeStaminaDamage(comp.Owner, damage / toHit.Count, comp); if (comp.StaminaDamage.Equals(oldDamage)) { _popup.PopupEntity(Loc.GetString("stamina-resist"), comp.Owner, Filter.Entities(args.User)); @@ -150,7 +150,7 @@ public sealed class StaminaSystem : EntitySystem _alerts.ShowAlert(uid, AlertType.Stamina, (short) severity); } - public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null, SoundSpecifier? knockdownSound = null) + public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null) { if (!Resolve(uid, ref component, false) || component.Critical) return; @@ -181,8 +181,6 @@ public sealed class StaminaSystem : EntitySystem { if (component.StaminaDamage >= component.CritThreshold) { - if (knockdownSound != null) - SoundSystem.Play(knockdownSound.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, knockdownSound.Params); EnterStamCrit(uid, component); } } diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 9d725ad6fe..7a53212830 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -21,6 +21,7 @@ using Content.Shared.StatusEffect; using Content.Shared.Stunnable; using Content.Shared.Tag; using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Events; diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 6c690cee77..970c2e196b 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -7,7 +7,6 @@ namespace Content.Server.Entry "ConstructionGhost", "IconSmooth", "InteractionOutline", - "MeleeWeaponArcAnimation", "AnimationsTest", "ItemStatus", "Marker", diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index d93234a51c..abd70c5a2e 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Flash.Components; using Content.Server.Light.EntitySystems; using Content.Server.Stunnable; -using Content.Server.Weapon.Melee; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Examine; using Content.Shared.Flash; using Content.Shared.IdentityManagement; diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 1adadbb44a..6294e55b3d 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -1,18 +1,14 @@ + + using Content.Server.Administration.Logs; -using Content.Server.Hands.Components; using Content.Server.Pulling; using Content.Server.Storage.Components; -using Content.Server.Weapon.Melee.Components; using Content.Shared.ActionBlocker; -using Content.Shared.Database; using Content.Shared.DragDrop; using Content.Shared.Input; using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Inventory; -using Content.Shared.Item; using Content.Shared.Pulling.Components; -using Content.Shared.Weapons.Melee; +using Content.Shared.Storage; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Containers; @@ -20,7 +16,6 @@ using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Players; using Robust.Shared.Random; -using static Content.Shared.Storage.SharedStorageComponent; namespace Content.Server.Interaction { @@ -34,9 +29,8 @@ namespace Content.Server.Interaction [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly PullingSystem _pullSystem = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - public override void Initialize() { @@ -61,7 +55,7 @@ namespace Content.Server.Interaction if (Deleted(target)) return false; - if (!target.TryGetContainer(out var container)) + if (!_container.TryGetContainingContainer(target, out var container)) return false; if (!TryComp(container.Owner, out ServerStorageComponent? storage)) @@ -74,7 +68,7 @@ namespace Content.Server.Interaction return false; // we don't check if the user can access the storage entity itself. This should be handed by the UI system. - return _uiSystem.SessionHasOpenUi(container.Owner, StorageUiKey.Key, actor.PlayerSession); + return _uiSystem.SessionHasOpenUi(container.Owner, SharedStorageComponent.StorageUiKey.Key, actor.PlayerSession); } #region Drag drop @@ -132,21 +126,6 @@ namespace Content.Server.Interaction } #endregion - /// - /// Entity will try and use their active hand at the target location. - /// Don't use for players - /// - /// - /// - /// - internal void AiUseInteraction(EntityUid entity, EntityCoordinates coords, EntityUid uid) - { - if (HasComp(entity)) - throw new InvalidOperationException(); - - UserInteraction(entity, coords, uid); - } - private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { if (!ValidateClientInput(session, coords, uid, out var userEntity)) @@ -169,103 +148,5 @@ namespace Content.Server.Interaction return _pullSystem.TogglePull(userEntity.Value, pull); } - - public override void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? target = null) - { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. - if (!ValidateInteractAndFace(user, coordinates)) - return; - - // Check general interaction blocking. - if (!_actionBlockerSystem.CanInteract(user, target)) - return; - - // Check combat-specific action blocking. - if (!_actionBlockerSystem.CanAttack(user, target)) - return; - - if (!wideAttack) - { - // Check if interacted entity is in the same container, the direct child, or direct parent of the user. - if (target != null && !Deleted(target.Value) && !ContainerSystem.IsInSameOrParentContainer(user, target.Value) && !CanAccessViaStorage(user, target.Value)) - { - Logger.WarningS("system.interaction", - $"User entity {ToPrettyString(user):user} clicked on object {ToPrettyString(target.Value):target} that isn't the parent, child, or in the same container"); - return; - } - - // TODO: Replace with body attack range when we get something like arm length or telekinesis or something. - var unobstructed = (target == null) - ? InRangeUnobstructed(user, coordinates) - : InRangeUnobstructed(user, target.Value); - - if (!unobstructed) - return; - } - else if (ContainerSystem.IsEntityInContainer(user)) - { - // No wide attacking while in containers (holos, lockers, etc). - // Can't think of a valid case where you would want this. - return; - } - - // Verify user has a hand, and find what object they are currently holding in their active hand - if (TryComp(user, out HandsComponent? hands)) - { - var item = hands.ActiveHandEntity; - - if (!Deleted(item)) - { - var meleeVee = new MeleeAttackAttemptEvent(); - RaiseLocalEvent(item.Value, ref meleeVee, true); - - if (meleeVee.Cancelled) return; - - if (wideAttack) - { - var ev = new WideAttackEvent(item.Value, user, coordinates); - RaiseLocalEvent(item.Value, ev, false); - - if (ev.Handled) - return; - } - else - { - var ev = new ClickAttackEvent(item.Value, user, coordinates, target); - RaiseLocalEvent(item.Value, ev, false); - - if (ev.Handled) - return; - } - } - else if (!wideAttack && target != null && HasComp(target.Value)) - { - // We pick up items if our hand is empty, even if we're in combat mode. - InteractHand(user, target.Value); - return; - } - } - - // TODO: Make this saner? - // Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter. - - var used = user; - - if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves) && HasComp(gloves)) - used = (EntityUid) gloves; - - if (wideAttack) - { - var ev = new WideAttackEvent(used, user, coordinates); - RaiseLocalEvent(used, ev, false); - if (ev.Handled) - _adminLogger.Add(LogType.AttackUnarmedWide, LogImpact.Low, $"{ToPrettyString(user):user} wide attacked at {coordinates}"); - } - else - { - var ev = new ClickAttackEvent(used, user, coordinates, target); - RaiseLocalEvent(used, ev, false); - } - } } } diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index e34d99dcd6..c125020e88 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -7,7 +7,7 @@ using Content.Server.Doors.Components; using Content.Server.Magic.Events; using Content.Server.Popups; using Content.Server.Spawners.Components; -using Content.Server.Weapon.Ranged.Systems; +using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using Content.Shared.Body.Components; diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs index d712e31847..60ac7f5083 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs @@ -1,8 +1,8 @@ using Content.Server.CombatMode; using Content.Server.NPC.Components; -using Content.Server.Weapon.Melee.Components; using Content.Shared.MobState; using Content.Shared.MobState.Components; +using Content.Shared.Weapons.Melee; namespace Content.Server.NPC.Systems; @@ -64,7 +64,7 @@ public sealed partial class NPCCombatSystem return; } - if (weapon.CooldownEnd > _timing.CurTime) + if (weapon.NextAttack > _timing.CurTime) { return; } @@ -84,6 +84,6 @@ public sealed partial class NPCCombatSystem return; } - _interaction.DoAttack(component.Owner, targetXform.Coordinates, false, component.Target); + _melee.AttemptLightAttack(component.Owner, weapon, component.Target); } } diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.cs b/Content.Server/NPC/Systems/NPCCombatSystem.cs index 4fc8197f46..2d0650dd5e 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.cs @@ -1,7 +1,8 @@ using Content.Server.Interaction; -using Content.Server.Weapon.Ranged.Systems; +using Content.Server.Weapons.Ranged.Systems; using Content.Shared.CombatMode; using Content.Shared.Interaction; +using Content.Shared.Weapons.Melee; using Robust.Shared.Map; using Robust.Shared.Timing; @@ -17,6 +18,7 @@ public sealed partial class NPCCombatSystem : EntitySystem [Dependency] private readonly GunSystem _gun = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() diff --git a/Content.Server/Projectiles/SharedProjectileSystem.cs b/Content.Server/Projectiles/SharedProjectileSystem.cs index ee73016b8b..2f4f655b58 100644 --- a/Content.Server/Projectiles/SharedProjectileSystem.cs +++ b/Content.Server/Projectiles/SharedProjectileSystem.cs @@ -1,19 +1,17 @@ using Content.Server.Administration.Logs; using Content.Server.Projectiles.Components; +using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Camera; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Projectiles; -using Content.Shared.Vehicle.Components; using Content.Shared.Weapons.Melee; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.GameStates; -using Robust.Shared.Physics.Dynamics; using Robust.Shared.Player; using Robust.Shared.Physics.Events; -using GunSystem = Content.Server.Weapon.Ranged.Systems.GunSystem; namespace Content.Server.Projectiles { @@ -52,7 +50,7 @@ namespace Content.Server.Projectiles { if (modifiedDamage.Total > FixedPoint2.Zero) { - RaiseNetworkEvent(new DamageEffectEvent(otherEntity), Filter.Pvs(otherEntity, entityManager: EntityManager)); + RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager)); } _adminLogger.Add(LogType.BulletHit, diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 5fa4b1d706..9b135f177a 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -28,6 +28,7 @@ using Content.Server.Popups; using Content.Shared.Destructible; using static Content.Shared.Storage.SharedStorageComponent; using Content.Shared.ActionBlocker; +using Content.Shared.CombatMode; using Content.Shared.Movement.Events; namespace Content.Server.Storage.EntitySystems @@ -48,6 +49,7 @@ namespace Content.Server.Storage.EntitySystems [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; /// @@ -263,6 +265,9 @@ namespace Content.Server.Storage.EntitySystems /// private void OnActivate(EntityUid uid, ServerStorageComponent storageComp, ActivateInWorldEvent args) { + if (args.Handled || _combatMode.IsInCombatMode(args.User)) + return; + if (TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) return; diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs index 5d1ec5897d..9cf044c0d2 100644 --- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs +++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs @@ -5,7 +5,7 @@ using Content.Server.Power.Components; using Content.Server.Power.Events; using Content.Server.Speech.EntitySystems; using Content.Server.Stunnable.Components; -using Content.Server.Weapon.Melee; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Audio; using Content.Shared.Examine; using Content.Shared.Interaction.Events; diff --git a/Content.Server/Tools/ToolSystem.Welder.cs b/Content.Server/Tools/ToolSystem.Welder.cs index 805c0bc948..139926d8ae 100644 --- a/Content.Server/Tools/ToolSystem.Welder.cs +++ b/Content.Server/Tools/ToolSystem.Welder.cs @@ -3,7 +3,7 @@ using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; using Content.Server.Tools.Components; -using Content.Server.Weapon.Melee; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Audio; using Content.Shared.Examine; using Content.Shared.FixedPoint; diff --git a/Content.Server/UserInterface/StatValuesCommand.cs b/Content.Server/UserInterface/StatValuesCommand.cs index 51f0a160fc..a507fd1fda 100644 --- a/Content.Server/UserInterface/StatValuesCommand.cs +++ b/Content.Server/UserInterface/StatValuesCommand.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Content.Server.Administration; using Content.Server.Cargo.Systems; using Content.Server.EUI; @@ -5,6 +6,7 @@ using Content.Shared.Administration; using Content.Shared.Materials; using Content.Shared.Research.Prototypes; using Content.Shared.UserInterface; +using Content.Shared.Weapons.Melee; using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -16,7 +18,7 @@ public sealed class StatValuesCommand : IConsoleCommand { public string Command => "showvalues"; public string Description => "Dumps all stats for a particular category into a table."; - public string Help => $"{Command} "; + public string Help => $"{Command} "; public void Execute(IConsoleShell shell, string argStr, string[] args) { if (shell.Player is not IPlayerSession pSession) @@ -41,6 +43,9 @@ public sealed class StatValuesCommand : IConsoleCommand case "lathesell": message = GetLatheMessage(); break; + case "melee": + message = GetMelee(); + break; default: shell.WriteError($"{args[0]} is not a valid stat!"); return; @@ -100,6 +105,51 @@ public sealed class StatValuesCommand : IConsoleCommand return state; } + private StatValuesEuiMessage GetMelee() + { + var compFactory = IoCManager.Resolve(); + var protoManager = IoCManager.Resolve(); + + var values = new List(); + + foreach (var proto in protoManager.EnumeratePrototypes()) + { + if (proto.Abstract || + !proto.Components.TryGetValue(compFactory.GetComponentName(typeof(MeleeWeaponComponent)), + out var meleeComp)) + { + continue; + } + + var comp = (MeleeWeaponComponent) meleeComp.Component; + + // TODO: Wielded damage + // TODO: Esword damage + + values.Add(new[] + { + proto.ID, + (comp.Damage.Total * comp.AttackRate).ToString(), + comp.AttackRate.ToString(CultureInfo.CurrentCulture), + comp.Damage.Total.ToString(), + comp.Range.ToString(CultureInfo.CurrentCulture), + }); + } + + var state = new StatValuesEuiMessage() + { + Title = "Cargo sell prices", + Headers = new List() + { + "ID", + "Price", + }, + Values = values, + }; + + return state; + } + private StatValuesEuiMessage GetLatheMessage() { var values = new List(); diff --git a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs b/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs deleted file mode 100644 index 4cf6b14df9..0000000000 --- a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Audio; -using Content.Shared.FixedPoint; - -namespace Content.Server.Weapon.Melee.Components -{ - [RegisterComponent] - public sealed class MeleeWeaponComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("hitSound")] - public SoundSpecifier? HitSound; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("noDamageSound")] - public SoundSpecifier NoDamageSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/tap.ogg"); - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("missSound")] - public SoundSpecifier MissSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg"); - - [ViewVariables] - [DataField("arcCooldownTime")] - public float ArcCooldownTime { get; } = 1f; - - [ViewVariables] - [DataField("cooldownTime")] - public float CooldownTime { get; } = 1f; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("clickArc")] - public string ClickArc { get; set; } = "punch"; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("arc")] - public string? Arc { get; set; } = "default"; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("arcwidth")] - public float ArcWidth { get; set; } = 90; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("range")] - public float Range { get; set; } = 1; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("clickAttackEffect")] - public bool ClickAttackEffect { get; set; } = true; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("hidden")] - public bool HideFromExamine { get; set; } = false; - - public TimeSpan LastAttackTime; - public TimeSpan CooldownEnd; - - [DataField("damage", required:true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier Damage = default!; - - [DataField("bluntStaminaDamageFactor")] - [ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 BluntStaminaDamageFactor { get; set; } = 0.5f; - } -} diff --git a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs deleted file mode 100644 index 692c40aee7..0000000000 --- a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs +++ /dev/null @@ -1,520 +0,0 @@ -using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.Body.Components; -using Content.Server.Body.Systems; -using Content.Server.Chemistry.Components; -using Content.Server.Chemistry.EntitySystems; -using Content.Server.Cooldown; -using Content.Server.Damage.Components; -using Content.Server.Damage.Systems; -using Content.Server.Examine; -using Content.Server.Weapon.Melee.Components; -using Content.Shared.Damage; -using Content.Shared.Audio; -using Content.Shared.Database; -using Content.Shared.Examine; -using Content.Shared.FixedPoint; -using Content.Shared.Hands; -using Content.Shared.Physics; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; - -namespace Content.Server.Weapon.Melee -{ - public sealed class MeleeWeaponSystem : EntitySystem - { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly ExamineSystem _examine = default!; - [Dependency] private readonly StaminaSystem _staminaSystem = default!; - [Dependency] private readonly SolutionContainerSystem _solutionsSystem = default!; - - [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; - - public const float DamagePitchVariation = 0.15f; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnHandSelected); - SubscribeLocalEvent(OnClickAttack); - SubscribeLocalEvent(OnWideAttack); - SubscribeLocalEvent>(OnMeleeExaminableVerb); - SubscribeLocalEvent(OnChemicalInjectorHit); - } - - private void OnMeleeExaminableVerb(EntityUid uid, MeleeWeaponComponent component, GetVerbsEvent args) - { - if (!args.CanInteract || !args.CanAccess || component.HideFromExamine) - return; - - var getDamage = new ItemMeleeDamageEvent(component.Damage); - RaiseLocalEvent(uid, getDamage, false); - - var damageSpec = GetDamage(component); - - if (damageSpec == null) - damageSpec = new DamageSpecifier(); - - damageSpec += getDamage.BonusDamage; - - if (damageSpec.Total == FixedPoint2.Zero) - return; - - var verb = new ExamineVerb() - { - Act = () => - { - var markup = _damageable.GetDamageExamine(damageSpec, Loc.GetString("damage-melee")); - _examine.SendExamineTooltip(args.User, uid, markup, false, false); - }, - Text = Loc.GetString("damage-examinable-verb-text"), - Message = Loc.GetString("damage-examinable-verb-message"), - Category = VerbCategory.Examine, - IconTexture = "/Textures/Interface/VerbIcons/smite.svg.192dpi.png" - }; - - args.Verbs.Add(verb); - } - - private DamageSpecifier? GetDamage(MeleeWeaponComponent component) - { - return component.Damage.Total > FixedPoint2.Zero ? component.Damage : null; - } - - private void OnHandSelected(EntityUid uid, MeleeWeaponComponent comp, HandSelectedEvent args) - { - var curTime = _gameTiming.CurTime; - var cool = TimeSpan.FromSeconds(comp.CooldownTime * 0.5f); - - if (curTime < comp.CooldownEnd) - { - if (comp.CooldownEnd - curTime < cool) - { - comp.LastAttackTime = curTime; - comp.CooldownEnd += cool; - } - else - return; - } - else - { - comp.LastAttackTime = curTime; - comp.CooldownEnd = curTime + cool; - } - - RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false); - } - - private void OnClickAttack(EntityUid owner, MeleeWeaponComponent comp, ClickAttackEvent args) - { - args.Handled = true; - var curTime = _gameTiming.CurTime; - - if (curTime < comp.CooldownEnd || - args.Target == null || - args.Target == owner || - args.User == args.Target) - return; - - var location = Transform(args.User).Coordinates; - var diff = args.ClickLocation.ToMapPos(EntityManager) - location.ToMapPos(EntityManager); - var angle = Angle.FromWorldVec(diff); - - if (args.Target is {Valid: true} target) - { - // Raising a melee hit event which may handle combat for us - var hitEvent = new MeleeHitEvent(new List() { target }, args.User, comp.Damage); - RaiseLocalEvent(owner, hitEvent, false); - - if (!hitEvent.Handled) - { - var targets = new[] { target }; - SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false); - - // Raising a GetMeleeDamage event which gets our damage - var getDamageEvent = new ItemMeleeDamageEvent(comp.Damage); - RaiseLocalEvent(owner, getDamageEvent, false); - - RaiseLocalEvent(target, new AttackedEvent(args.Used, args.User, args.ClickLocation), true); - - var modifiersList = getDamageEvent.ModifiersList; - modifiersList.AddRange(hitEvent.ModifiersList); - var modifiedDamage = DamageSpecifier.ApplyModifierSets(comp.Damage + hitEvent.BonusDamage + getDamageEvent.BonusDamage, modifiersList); - var damageResult = _damageable.TryChangeDamage(target, modifiedDamage); - - if (damageResult != null && damageResult.Total > FixedPoint2.Zero) - { - FixedPoint2 bluntDamage; - // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor - if (damageResult.DamageDict.TryGetValue("Blunt", out bluntDamage)) - { - _staminaSystem.TakeStaminaDamage(target, (bluntDamage * comp.BluntStaminaDamageFactor).Float()); - } - - if (args.Used == args.User) - _adminLogger.Add(LogType.MeleeHit, - $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(args.Target.Value):target} using their hands and dealt {damageResult.Total:damage} damage"); - else - _adminLogger.Add(LogType.MeleeHit, - $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(args.Target.Value):target} using {ToPrettyString(args.Used):used} and dealt {damageResult.Total:damage} damage"); - - PlayHitSound(target, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, comp.HitSound); - } - else - { - SoundSystem.Play((hitEvent.HitSoundOverride != null) - ? hitEvent.HitSoundOverride.GetSound() - : comp.NoDamageSound.GetSound(), Filter.Pvs(owner, entityManager: EntityManager), owner); - } - } - } - else - { - SoundSystem.Play(comp.MissSound.GetSound(), Filter.Pvs(owner, entityManager: EntityManager), owner); - } - - comp.LastAttackTime = curTime; - SetAttackCooldown(owner, comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime), comp); - - RaiseLocalEvent(owner, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd)); - } - - /// - /// Set the melee weapon cooldown's end to the specified value. Will use the maximum of the existing cooldown or the new one. - /// - public void SetAttackCooldown(EntityUid uid, TimeSpan endTime, MeleeWeaponComponent? component = null) - { - // Some other system may want to artificially inflate melee weapon CD. - if (!Resolve(uid, ref component) || component.CooldownEnd > endTime) return; - - component.CooldownEnd = endTime; - RaiseLocalEvent(uid, new RefreshItemCooldownEvent(component.LastAttackTime, component.CooldownEnd)); - } - - private void OnWideAttack(EntityUid owner, MeleeWeaponComponent comp, WideAttackEvent args) - { - if (string.IsNullOrEmpty(comp.Arc)) return; - - args.Handled = true; - var curTime = _gameTiming.CurTime; - - if (curTime < comp.CooldownEnd) - { - return; - } - - var location = EntityManager.GetComponent(args.User).Coordinates; - var diff = args.ClickLocation.ToMapPos(EntityManager) - location.ToMapPos(EntityManager); - var angle = Angle.FromWorldVec(diff); - - // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. - var entities = ArcRayCast(EntityManager.GetComponent(args.User).WorldPosition, angle, comp.ArcWidth, comp.Range, EntityManager.GetComponent(owner).MapID, args.User); - - var hitEntities = new List(); - foreach (var entity in entities) - { - if (entity.IsInContainer() || entity == args.User) - continue; - - if (EntityManager.HasComponent(entity)) - { - hitEntities.Add(entity); - } - } - // Raising a melee hit event which may handle combat for us - var hitEvent = new MeleeHitEvent(hitEntities, args.User, comp.Damage); - RaiseLocalEvent(owner, hitEvent, false); - SendAnimation(comp.Arc, angle, args.User, owner, hitEntities); - - if (!hitEvent.Handled) - { - var getDamageEvent = new ItemMeleeDamageEvent(comp.Damage); - RaiseLocalEvent(owner, getDamageEvent, false); - - var modifiersList = getDamageEvent.ModifiersList; - modifiersList.AddRange(hitEvent.ModifiersList); - var modifiedDamage = DamageSpecifier.ApplyModifierSets(comp.Damage + hitEvent.BonusDamage + getDamageEvent.BonusDamage, modifiersList); - var appliedDamage = new DamageSpecifier(); - - foreach (var entity in hitEntities) - { - RaiseLocalEvent(entity, new AttackedEvent(args.Used, args.User, args.ClickLocation), true); - - var damageResult = _damageable.TryChangeDamage(entity, modifiedDamage); - - if (damageResult != null && damageResult.Total > FixedPoint2.Zero) - { - appliedDamage += damageResult; - - if (args.Used == args.User) - _adminLogger.Add(LogType.MeleeHit, - $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(entity):target} using their hands and dealt {damageResult.Total:damage} damage"); - else - _adminLogger.Add(LogType.MeleeHit, - $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(entity):target} using {ToPrettyString(args.Used):used} and dealt {damageResult.Total:damage} damage"); - } - } - - if (entities.Count != 0) - { - if (appliedDamage.Total > FixedPoint2.Zero) - { - var target = entities.First(); - PlayHitSound(target, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, comp.HitSound); - } - else - { - SoundSystem.Play((hitEvent.HitSoundOverride != null) - ? hitEvent.HitSoundOverride.GetSound() - : comp.NoDamageSound.GetSound(), Filter.Pvs(owner, entityManager: EntityManager), owner); - } - } - else - { - SoundSystem.Play(comp.MissSound.GetSound(), Filter.Pvs(owner, entityManager: EntityManager), owner); - } - } - - comp.LastAttackTime = curTime; - comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.ArcCooldownTime); - RaiseLocalEvent(owner, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd)); - } - - public static string? GetHighestDamageSound(DamageSpecifier modifiedDamage, IPrototypeManager protoManager) - { - var groups = modifiedDamage.GetDamagePerGroup(protoManager); - - // Use group if it's exclusive, otherwise fall back to type. - if (groups.Count == 1) - { - return groups.Keys.First(); - } - - var highestDamage = FixedPoint2.Zero; - string? highestDamageType = null; - - foreach (var (type, damage) in modifiedDamage.DamageDict) - { - if (damage <= highestDamage) continue; - highestDamageType = type; - } - - return highestDamageType; - } - - private void PlayHitSound(EntityUid target, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound) - { - var playedSound = false; - - // Play sound based off of highest damage type. - if (TryComp(target, out var damageSoundComp)) - { - if (type == null && damageSoundComp.NoDamageSound != null) - { - SoundSystem.Play(damageSoundComp.NoDamageSound.GetSound(), Filter.Pvs(target, entityManager: EntityManager), target, AudioHelpers.WithVariation(DamagePitchVariation)); - playedSound = true; - } - else if (type != null && damageSoundComp.SoundTypes?.TryGetValue(type, out var damageSoundType) == true) - { - SoundSystem.Play(damageSoundType.GetSound(), Filter.Pvs(target, entityManager: EntityManager), target, AudioHelpers.WithVariation(DamagePitchVariation)); - playedSound = true; - } - else if (type != null && damageSoundComp.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true) - { - SoundSystem.Play(damageSoundGroup.GetSound(), Filter.Pvs(target, entityManager: EntityManager), target, AudioHelpers.WithVariation(DamagePitchVariation)); - playedSound = true; - } - } - - // Use weapon sounds if the thing being hit doesn't specify its own sounds. - if (!playedSound) - { - if (hitSoundOverride != null) - { - SoundSystem.Play(hitSoundOverride.GetSound(), Filter.Pvs(target, entityManager: EntityManager), target, AudioHelpers.WithVariation(DamagePitchVariation)); - playedSound = true; - } - else if (hitSound != null) - { - SoundSystem.Play(hitSound.GetSound(), Filter.Pvs(target, entityManager: EntityManager), target); - playedSound = true; - } - } - - // Fallback to generic sounds. - if (!playedSound) - { - switch (type) - { - // Unfortunately heat returns caustic group so can't just use the damagegroup in that instance. - case "Burn": - case "Heat": - case "Cold": - SoundSystem.Play("/Audio/Items/welder.ogg", Filter.Pvs(target, entityManager: EntityManager), target); - break; - // No damage, fallback to tappies - case null: - SoundSystem.Play("/Audio/Weapons/tap.ogg", Filter.Pvs(target, entityManager: EntityManager), target); - break; - case "Brute": - SoundSystem.Play("/Audio/Weapons/smash.ogg", Filter.Pvs(target, entityManager: EntityManager), target); - break; - } - } - } - - private HashSet ArcRayCast(Vector2 position, Angle angle, float arcWidth, float range, MapId mapId, EntityUid ignore) - { - var widthRad = Angle.FromDegrees(arcWidth); - var increments = 1 + 35 * (int) Math.Ceiling(widthRad / (2 * Math.PI)); - var increment = widthRad / increments; - var baseAngle = angle - widthRad / 2; - - var resSet = new HashSet(); - - for (var i = 0; i < increments; i++) - { - var castAngle = new Angle(baseAngle + increment * i); - var res = Get().IntersectRay(mapId, - new CollisionRay(position, castAngle.ToWorldVec(), - (int) (CollisionGroup.MobMask | CollisionGroup.Opaque)), range, ignore).ToList(); - - if (res.Count != 0) - { - resSet.Add(res[0].HitEntity); - } - } - - return resSet; - } - - private void OnChemicalInjectorHit(EntityUid owner, MeleeChemicalInjectorComponent comp, MeleeHitEvent args) - { - if (!_solutionsSystem.TryGetInjectableSolution(owner, out var solutionContainer)) - return; - - var hitBloodstreams = new List(); - foreach (var entity in args.HitEntities) - { - if (Deleted(entity)) - continue; - - if (EntityManager.TryGetComponent(entity, out var bloodstream)) - hitBloodstreams.Add(bloodstream); - } - - if (hitBloodstreams.Count < 1) - return; - - var removedSolution = solutionContainer.SplitSolution(comp.TransferAmount * hitBloodstreams.Count); - var removedVol = removedSolution.TotalVolume; - var solutionToInject = removedSolution.SplitSolution(removedVol * comp.TransferEfficiency); - var volPerBloodstream = solutionToInject.TotalVolume * (1 / hitBloodstreams.Count); - - foreach (var bloodstream in hitBloodstreams) - { - var individualInjection = solutionToInject.SplitSolution(volPerBloodstream); - _bloodstreamSystem.TryAddToChemicals((bloodstream).Owner, individualInjection, bloodstream); - } - } - - public void SendAnimation(string arc, Angle angle, EntityUid attacker, EntityUid source, IEnumerable hits, bool textureEffect = false, bool arcFollowAttacker = true) - { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker, source, - hits.Select(e => e).ToList(), textureEffect, arcFollowAttacker), Filter.Pvs(source, 1f)); - } - - public void SendLunge(Angle angle, EntityUid source) - { - RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayLungeAnimationMessage(angle, source), Filter.Pvs(source, 1f)); - } - } - - public sealed class ItemMeleeDamageEvent : HandledEntityEventArgs - { - /// - /// The base amount of damage dealt by the melee hit. - /// - public readonly DamageSpecifier BaseDamage = new(); - - /// - /// Modifier sets to apply to the damage when it's all said and done. - /// This should be modified by adding a new entry to the list. - /// - public List ModifiersList = new(); - - /// - /// Damage to add to the default melee weapon damage. Applied before modifiers. - /// - /// - /// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier. - /// - public DamageSpecifier BonusDamage = new(); - - public ItemMeleeDamageEvent(DamageSpecifier baseDamage) - { - BaseDamage = baseDamage; - } - } - - /// - /// Raised directed on the melee weapon entity used to attack something in combat mode, - /// whether through a click attack or wide attack. - /// - public sealed class MeleeHitEvent : HandledEntityEventArgs - { - /// - /// The base amount of damage dealt by the melee hit. - /// - public readonly DamageSpecifier BaseDamage = new(); - - /// - /// Modifier sets to apply to the hit event when it's all said and done. - /// This should be modified by adding a new entry to the list. - /// - public List ModifiersList = new(); - - /// - /// Damage to add to the default melee weapon damage. Applied before modifiers. - /// - /// - /// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier. - /// - public DamageSpecifier BonusDamage = new(); - - /// - /// A list containing every hit entity. Can be zero. - /// - public IEnumerable HitEntities { get; } - - /// - /// Used to define a new hit sound in case you want to override the default GenericHit. - /// Also gets a pitch modifier added to it. - /// - public SoundSpecifier? HitSoundOverride {get; set;} - - /// - /// The user who attacked with the melee weapon. - /// - public EntityUid User { get; } - - public MeleeHitEvent(List hitEntities, EntityUid user, DamageSpecifier baseDamage) - { - HitEntities = hitEntities; - User = user; - BaseDamage = baseDamage; - } - } -} diff --git a/Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs b/Content.Server/Weapons/Melee/Components/MeleeSoundComponent.cs similarity index 96% rename from Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs rename to Content.Server/Weapons/Melee/Components/MeleeSoundComponent.cs index a8a6fcbe83..23b2c2eb4d 100644 --- a/Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs +++ b/Content.Server/Weapons/Melee/Components/MeleeSoundComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -namespace Content.Server.Weapon.Melee.Components; +namespace Content.Server.Weapons.Melee.Components; /// /// Plays the specified sound upon receiving damage of the specified type. diff --git a/Content.Server/Weapon/Melee/EnergySword/Components/EnergySwordComponent.cs b/Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs similarity index 96% rename from Content.Server/Weapon/Melee/EnergySword/Components/EnergySwordComponent.cs rename to Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs index 9de9c6449a..b482c6c738 100644 --- a/Content.Server/Weapon/Melee/EnergySword/Components/EnergySwordComponent.cs +++ b/Content.Server/Weapons/Melee/EnergySword/Components/EnergySwordComponent.cs @@ -1,7 +1,7 @@ using Content.Shared.Damage; using Robust.Shared.Audio; -namespace Content.Server.Weapon.Melee.EnergySword +namespace Content.Server.Weapons.Melee.EnergySword.Components { [RegisterComponent] internal sealed class EnergySwordComponent : Component diff --git a/Content.Server/Weapon/Melee/EnergySword/EnergySwordSystem.cs b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs similarity index 96% rename from Content.Server/Weapon/Melee/EnergySword/EnergySwordSystem.cs rename to Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs index 0db06cd01e..a2a13f564c 100644 --- a/Content.Server/Weapon/Melee/EnergySword/EnergySwordSystem.cs +++ b/Content.Server/Weapons/Melee/EnergySword/EnergySwordSystem.cs @@ -1,6 +1,7 @@ using Content.Server.CombatMode.Disarm; using Content.Server.Kitchen.Components; -using Content.Server.Weapon.Melee.Components; +using Content.Server.Weapons.Melee.EnergySword.Components; +using Content.Server.Weapons.Melee.Events; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Item; @@ -9,11 +10,12 @@ using Content.Shared.Light.Component; using Content.Shared.Temperature; using Content.Shared.Toggleable; using Content.Shared.Tools.Components; +using Content.Shared.Weapons.Melee; using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Random; -namespace Content.Server.Weapon.Melee.EnergySword +namespace Content.Server.Weapons.Melee.EnergySword { public sealed class EnergySwordSystem : EntitySystem { diff --git a/Content.Server/Weapons/Melee/Events/ItemMeleeDamageEvent.cs b/Content.Server/Weapons/Melee/Events/ItemMeleeDamageEvent.cs new file mode 100644 index 0000000000..52a60c5fa6 --- /dev/null +++ b/Content.Server/Weapons/Melee/Events/ItemMeleeDamageEvent.cs @@ -0,0 +1,30 @@ +using Content.Shared.Damage; + +namespace Content.Server.Weapons.Melee.Events; + +public sealed class ItemMeleeDamageEvent : HandledEntityEventArgs +{ + /// + /// The base amount of damage dealt by the melee hit. + /// + public readonly DamageSpecifier BaseDamage = new(); + + /// + /// Modifier sets to apply to the damage when it's all said and done. + /// This should be modified by adding a new entry to the list. + /// + public List ModifiersList = new(); + + /// + /// Damage to add to the default melee weapon damage. Applied before modifiers. + /// + /// + /// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier. + /// + public DamageSpecifier BonusDamage = new(); + + public ItemMeleeDamageEvent(DamageSpecifier baseDamage) + { + BaseDamage = baseDamage; + } +} diff --git a/Content.Server/Weapons/Melee/Events/MeleeHitEvent.cs b/Content.Server/Weapons/Melee/Events/MeleeHitEvent.cs new file mode 100644 index 0000000000..7a3772d8fc --- /dev/null +++ b/Content.Server/Weapons/Melee/Events/MeleeHitEvent.cs @@ -0,0 +1,53 @@ +using Content.Shared.Damage; +using Robust.Shared.Audio; + +namespace Content.Server.Weapons.Melee.Events; + +/// +/// Raised directed on the melee weapon entity used to attack something in combat mode, +/// whether through a click attack or wide attack. +/// +public sealed class MeleeHitEvent : HandledEntityEventArgs +{ + /// + /// The base amount of damage dealt by the melee hit. + /// + public readonly DamageSpecifier BaseDamage = new(); + + /// + /// Modifier sets to apply to the hit event when it's all said and done. + /// This should be modified by adding a new entry to the list. + /// + public List ModifiersList = new(); + + /// + /// Damage to add to the default melee weapon damage. Applied before modifiers. + /// + /// + /// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier. + /// + public DamageSpecifier BonusDamage = new(); + + /// + /// A list containing every hit entity. Can be zero. + /// + public IEnumerable HitEntities { get; } + + /// + /// Used to define a new hit sound in case you want to override the default GenericHit. + /// Also gets a pitch modifier added to it. + /// + public SoundSpecifier? HitSoundOverride {get; set;} + + /// + /// The user who attacked with the melee weapon. + /// + public EntityUid User { get; } + + public MeleeHitEvent(List hitEntities, EntityUid user, DamageSpecifier baseDamage) + { + HitEntities = hitEntities; + User = user; + BaseDamage = baseDamage; + } +} \ No newline at end of file diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs new file mode 100644 index 0000000000..53e3ab6c9d --- /dev/null +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -0,0 +1,536 @@ +using System.Linq; +using Content.Server.Actions.Events; +using Content.Server.Administration.Components; +using Content.Server.Administration.Logs; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Chemistry.Components; +using Content.Server.Chemistry.EntitySystems; +using Content.Server.CombatMode; +using Content.Server.CombatMode.Disarm; +using Content.Server.Contests; +using Content.Server.Damage.Systems; +using Content.Server.Examine; +using Content.Server.Hands.Components; +using Content.Server.Weapons.Melee.Components; +using Content.Server.Weapons.Melee.Events; +using Content.Shared.CombatMode; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Physics; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Audio; +using Robust.Shared.Map; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Weapons.Melee; + +public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem +{ + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly BloodstreamSystem _bloodstream = default!; + [Dependency] private readonly ContestsSystem _contests = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly ExamineSystem _examine = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SolutionContainerSystem _solutions = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + + public const float DamagePitchVariation = 0.05f; + + private const int AttackMask = (int) (CollisionGroup.MobMask | CollisionGroup.Opaque); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnChemicalInjectorHit); + SubscribeLocalEvent>(OnMeleeExaminableVerb); + } + + private void OnMeleeExaminableVerb(EntityUid uid, MeleeWeaponComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || component.HideFromExamine) + return; + + var getDamage = new ItemMeleeDamageEvent(component.Damage); + RaiseLocalEvent(uid, getDamage); + + var damageSpec = GetDamage(component); + + if (damageSpec == null) + damageSpec = new DamageSpecifier(); + + damageSpec += getDamage.BonusDamage; + + if (damageSpec.Total == FixedPoint2.Zero) + return; + + var verb = new ExamineVerb() + { + Act = () => + { + var markup = _damageable.GetDamageExamine(damageSpec, Loc.GetString("damage-melee")); + _examine.SendExamineTooltip(args.User, uid, markup, false, false); + }, + Text = Loc.GetString("damage-examinable-verb-text"), + Message = Loc.GetString("damage-examinable-verb-message"), + Category = VerbCategory.Examine, + IconTexture = "/Textures/Interface/VerbIcons/smite.svg.192dpi.png" + }; + + args.Verbs.Add(verb); + } + + private DamageSpecifier? GetDamage(MeleeWeaponComponent component) + { + return component.Damage.Total > FixedPoint2.Zero ? component.Damage : null; + } + + protected override void Popup(string message, EntityUid? uid, EntityUid? user) + { + if (uid == null) + return; + + PopupSystem.PopupEntity(message, uid.Value, Filter.Pvs(uid.Value, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user)); + } + + protected override void DoLightAttack(EntityUid user, LightAttackEvent ev, MeleeWeaponComponent component) + { + base.DoLightAttack(user, ev, component); + + // Can't attack yourself + // Not in LOS. + if (user == ev.Target || + ev.Target == null || + Deleted(ev.Target) || + // For consistency with wide attacks stuff needs damageable. + !HasComp(ev.Target) || + !TryComp(ev.Target, out var targetXform)) + { + return; + } + + // InRangeUnobstructed is insufficient rn as it checks the centre of the body rather than the nearest edge. + // TODO: Look at fixing it + // This is mainly to keep consistency between the wide attack raycast and the click attack raycast. + if (!_interaction.InRangeUnobstructed(user, ev.Target.Value, component.Range + 0.35f)) + return; + + var damage = component.Damage * GetModifier(component, true); + // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); + + // Raise event before doing damage so we can cancel damage if the event is handled + var hitEvent = new MeleeHitEvent(new List { ev.Target.Value }, user, damage); + RaiseLocalEvent(component.Owner, hitEvent); + + if (hitEvent.Handled) + return; + + var targets = new List(1) + { + ev.Target.Value + }; + + // For stuff that cares about it being attacked. + RaiseLocalEvent(ev.Target.Value, new AttackedEvent(component.Owner, user, targetXform.Coordinates)); + + var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList); + var damageResult = _damageable.TryChangeDamage(ev.Target, modifiedDamage); + + if (damageResult != null && damageResult.Total > FixedPoint2.Zero) + { + // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor + if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage)) + { + _stamina.TakeStaminaDamage(ev.Target.Value, (bluntDamage * component.BluntStaminaDamageFactor).Float()); + } + + if (component.Owner == user) + { + _adminLogger.Add(LogType.MeleeHit, + $"{ToPrettyString(user):user} melee attacked {ToPrettyString(ev.Target.Value):target} using their hands and dealt {damageResult.Total:damage} damage"); + } + else + { + _adminLogger.Add(LogType.MeleeHit, + $"{ToPrettyString(user):user} melee attacked {ToPrettyString(ev.Target.Value):target} using {ToPrettyString(component.Owner):used} and dealt {damageResult.Total:damage} damage"); + } + + PlayHitSound(ev.Target.Value, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound); + } + else + { + if (hitEvent.HitSoundOverride != null) + { + Audio.PlayPvs(hitEvent.HitSoundOverride, component.Owner); + } + else + { + Audio.PlayPvs(component.NoDamageSound, component.Owner); + } + } + + if (damageResult?.Total > FixedPoint2.Zero) + { + RaiseNetworkEvent(new DamageEffectEvent(Color.Red, targets), Filter.Pvs(targetXform.Coordinates, entityMan: EntityManager)); + } + } + + protected override void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, MeleeWeaponComponent component) + { + base.DoHeavyAttack(user, ev, component); + + // TODO: This is copy-paste as fuck with DoPreciseAttack + if (!TryComp(user, out var userXform)) + { + return; + } + + var targetMap = ev.Coordinates.ToMap(EntityManager); + + if (targetMap.MapId != userXform.MapID) + { + return; + } + + var userPos = userXform.WorldPosition; + var direction = targetMap.Position - userPos; + var distance = Math.Min(component.Range, direction.Length); + + // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. + var entities = ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user); + + if (entities.Count == 0) + return; + + var targets = new List(); + var damageQuery = GetEntityQuery(); + + foreach (var entity in entities) + { + if (entity == user || + !damageQuery.HasComponent(entity)) + continue; + + targets.Add(entity); + } + + var damage = component.Damage * GetModifier(component, false); + // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); + + // Raise event before doing damage so we can cancel damage if the event is handled + var hitEvent = new MeleeHitEvent(targets, user, damage); + RaiseLocalEvent(component.Owner, hitEvent); + + if (hitEvent.Handled) + return; + + // For stuff that cares about it being attacked. + foreach (var target in targets) + { + RaiseLocalEvent(target, new AttackedEvent(component.Owner, user, Transform(target).Coordinates)); + } + + var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList); + var appliedDamage = new DamageSpecifier(); + + foreach (var entity in targets) + { + RaiseLocalEvent(entity, new AttackedEvent(component.Owner, user, ev.Coordinates)); + + var damageResult = _damageable.TryChangeDamage(entity, modifiedDamage); + + if (damageResult != null && damageResult.Total > FixedPoint2.Zero) + { + appliedDamage += damageResult; + + if (component.Owner == user) + { + _adminLogger.Add(LogType.MeleeHit, + $"{ToPrettyString(user):user} melee attacked {ToPrettyString(entity):target} using their hands and dealt {damageResult.Total:damage} damage"); + } + else + { + _adminLogger.Add(LogType.MeleeHit, + $"{ToPrettyString(user):user} melee attacked {ToPrettyString(entity):target} using {ToPrettyString(component.Owner):used} and dealt {damageResult.Total:damage} damage"); + } + } + } + + if (entities.Count != 0) + { + if (appliedDamage.Total > FixedPoint2.Zero) + { + var target = entities.First(); + PlayHitSound(target, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound); + } + else + { + if (hitEvent.HitSoundOverride != null) + { + Audio.PlayPvs(hitEvent.HitSoundOverride, component.Owner); + } + else + { + Audio.PlayPvs(component.NoDamageSound, component.Owner); + } + } + } + + if (appliedDamage.Total > FixedPoint2.Zero) + { + RaiseNetworkEvent(new DamageEffectEvent(Color.Red, targets), Filter.Pvs(Transform(targets[0]).Coordinates, entityMan: EntityManager)); + } + } + + protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component) + { + if (!base.DoDisarm(user, ev, component)) + return false; + + if (!TryComp(user, out var combatMode)) + return false; + + var target = ev.Target!.Value; + + if (!TryComp(ev.Target.Value, out var targetHandsComponent)) + { + // Client will have already predicted this. + return false; + } + + if (!_interaction.InRangeUnobstructed(user, ev.Target.Value, component.Range + 0.1f)) + { + return false; + } + + EntityUid? inTargetHand = null; + + if (targetHandsComponent.ActiveHand is { IsEmpty: false }) + { + inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value; + } + + var attemptEvent = new DisarmAttemptEvent(target, user, inTargetHand); + + if (inTargetHand != null) + { + RaiseLocalEvent(inTargetHand.Value, attemptEvent); + } + + RaiseLocalEvent(target, attemptEvent); + + if (attemptEvent.Cancelled) + return false; + + var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode); + + if (_random.Prob(chance)) + { + // Don't play a sound as the swing is already predicted. + // Also don't play popups because most disarms will miss. + return false; + } + + var filterOther = Filter.Pvs(user, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user); + + var msgOther = Loc.GetString( + "disarm-action-popup-message-other-clients", + ("performerName", Identity.Entity(user, EntityManager)), + ("targetName", Identity.Entity(target, EntityManager))); + + var msgUser = Loc.GetString("disarm-action-popup-message-cursor", ("targetName", Identity.Entity(target, EntityManager))); + + PopupSystem.PopupEntity(msgOther, user, filterOther); + PopupSystem.PopupEntity(msgUser, target, Filter.Entities(user)); + + Audio.PlayPvs(combatMode.DisarmSuccessSound, user, AudioParams.Default.WithVariation(0.025f).WithVolume(5f)); + _adminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}"); + + var eventArgs = new DisarmedEvent { Target = target, Source = user, PushProbability = 1 - chance }; + RaiseLocalEvent(target, eventArgs); + + RaiseNetworkEvent(new DamageEffectEvent(Color.Aqua, new List() {target})); + return true; + } + + private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, SharedCombatModeComponent disarmerComp) + { + if (HasComp(disarmer)) + return 1.0f; + + if (HasComp(disarmed)) + return 0.0f; + + var contestResults = 1 - _contests.OverallStrengthContest(disarmer, disarmed); + + float chance = (disarmerComp.BaseDisarmFailChance + contestResults); + + if (inTargetHand != null && TryComp(inTargetHand, out var malus)) + { + chance += malus.Malus; + } + + return Math.Clamp(chance, 0f, 1f); + } + + private HashSet ArcRayCast(Vector2 position, Angle angle, Angle arcWidth, float range, MapId mapId, EntityUid ignore) + { + // TODO: This is pretty sucky. + var widthRad = arcWidth; + var increments = 1 + 35 * (int) Math.Ceiling(widthRad / (2 * Math.PI)); + var increment = widthRad / increments; + var baseAngle = angle - widthRad / 2; + + var resSet = new HashSet(); + + for (var i = 0; i < increments; i++) + { + var castAngle = new Angle(baseAngle + increment * i); + var res = _physics.IntersectRay(mapId, + new CollisionRay(position, castAngle.ToWorldVec(), + AttackMask), range, ignore, false).ToList(); + + if (res.Count != 0) + { + resSet.Add(res[0].HitEntity); + } + } + + return resSet; + } + + public override void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation) + { + RaiseNetworkEvent(new MeleeLungeEvent(user, angle, localPos, animation), Filter.Pvs(user, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user)); + } + + private void PlayHitSound(EntityUid target, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound) + { + var playedSound = false; + + // Play sound based off of highest damage type. + if (TryComp(target, out var damageSoundComp)) + { + if (type == null && damageSoundComp.NoDamageSound != null) + { + Audio.PlayPvs(damageSoundComp.NoDamageSound, target, AudioParams.Default.WithVariation(DamagePitchVariation)); + playedSound = true; + } + else if (type != null && damageSoundComp.SoundTypes?.TryGetValue(type, out var damageSoundType) == true) + { + Audio.PlayPvs(damageSoundType, target, AudioParams.Default.WithVariation(DamagePitchVariation)); + playedSound = true; + } + else if (type != null && damageSoundComp.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true) + { + Audio.PlayPvs(damageSoundGroup, target, AudioParams.Default.WithVariation(DamagePitchVariation)); + playedSound = true; + } + } + + // Use weapon sounds if the thing being hit doesn't specify its own sounds. + if (!playedSound) + { + if (hitSoundOverride != null) + { + Audio.PlayPvs(hitSoundOverride, target, AudioParams.Default.WithVariation(DamagePitchVariation)); + playedSound = true; + } + else if (hitSound != null) + { + Audio.PlayPvs(hitSound, target, AudioParams.Default.WithVariation(DamagePitchVariation)); + playedSound = true; + } + } + + // Fallback to generic sounds. + if (!playedSound) + { + switch (type) + { + // Unfortunately heat returns caustic group so can't just use the damagegroup in that instance. + case "Burn": + case "Heat": + case "Cold": + Audio.PlayPvs(new SoundPathSpecifier("/Audio/Items/welder.ogg"), target, AudioParams.Default.WithVariation(DamagePitchVariation)); + break; + // No damage, fallback to tappies + case null: + Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/tap.ogg"), target, AudioParams.Default.WithVariation(DamagePitchVariation)); + break; + case "Brute": + Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/smash.ogg"), target, AudioParams.Default.WithVariation(DamagePitchVariation)); + break; + } + } + } + + public static string? GetHighestDamageSound(DamageSpecifier modifiedDamage, IPrototypeManager protoManager) + { + var groups = modifiedDamage.GetDamagePerGroup(protoManager); + + // Use group if it's exclusive, otherwise fall back to type. + if (groups.Count == 1) + { + return groups.Keys.First(); + } + + var highestDamage = FixedPoint2.Zero; + string? highestDamageType = null; + + foreach (var (type, damage) in modifiedDamage.DamageDict) + { + if (damage <= highestDamage) + continue; + + highestDamageType = type; + } + + return highestDamageType; + } + + private void OnChemicalInjectorHit(EntityUid owner, MeleeChemicalInjectorComponent comp, MeleeHitEvent args) + { + if (!_solutions.TryGetInjectableSolution(owner, out var solutionContainer)) + return; + + var hitBloodstreams = new List(); + var bloodQuery = GetEntityQuery(); + + foreach (var entity in args.HitEntities) + { + if (Deleted(entity)) + continue; + + if (bloodQuery.TryGetComponent(entity, out var bloodstream)) + hitBloodstreams.Add(bloodstream); + } + + if (!hitBloodstreams.Any()) + return; + + var removedSolution = solutionContainer.SplitSolution(comp.TransferAmount * hitBloodstreams.Count); + var removedVol = removedSolution.TotalVolume; + var solutionToInject = removedSolution.SplitSolution(removedVol * comp.TransferEfficiency); + var volPerBloodstream = solutionToInject.TotalVolume * (1 / hitBloodstreams.Count); + + foreach (var bloodstream in hitBloodstreams) + { + var individualInjection = solutionToInject.SplitSolution(volPerBloodstream); + _bloodstream.TryAddToChemicals((bloodstream).Owner, individualInjection, bloodstream); + } + } +} diff --git a/Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs b/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs similarity index 73% rename from Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs rename to Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs index 8d38896abc..7c7a606de9 100644 --- a/Content.Server/Weapon/Ranged/Components/AmmoCounterComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/AmmoCounterComponent.cs @@ -1,6 +1,6 @@ using Content.Shared.Weapons.Ranged.Components; -namespace Content.Server.Weapon.Ranged.Components; +namespace Content.Server.Weapons.Ranged.Components; [RegisterComponent] public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {} diff --git a/Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs b/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs similarity index 83% rename from Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs rename to Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs index ce81de0a43..87d3ed7612 100644 --- a/Content.Server/Weapon/Ranged/Components/ChemicalAmmoComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/ChemicalAmmoComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Weapon.Ranged.Components +namespace Content.Server.Weapons.Ranged.Components { [RegisterComponent] public sealed class ChemicalAmmoComponent : Component diff --git a/Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs b/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs similarity index 95% rename from Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs rename to Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs index 118a820093..ffbee3ac6f 100644 --- a/Content.Server/Weapon/Ranged/Components/RangedDamageSoundComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/RangedDamageSoundComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes; using Robust.Shared.Audio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; -namespace Content.Server.Weapon.Ranged.Components; +namespace Content.Server.Weapons.Ranged.Components; /// /// Plays the specified sound upon receiving damage of that type. diff --git a/Content.Server/Weapon/Ranged/Components/RechargeBasicEntityAmmoComponent.cs b/Content.Server/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs similarity index 94% rename from Content.Server/Weapon/Ranged/Components/RechargeBasicEntityAmmoComponent.cs rename to Content.Server/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs index 05a15213ea..61f4815485 100644 --- a/Content.Server/Weapon/Ranged/Components/RechargeBasicEntityAmmoComponent.cs +++ b/Content.Server/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs @@ -1,6 +1,6 @@ using Robust.Shared.Audio; -namespace Content.Server.Weapon.Ranged.Components; +namespace Content.Server.Weapons.Ranged.Components; /// /// Responsible for handling recharging a basic entity ammo provider over time. diff --git a/Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs b/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs similarity index 94% rename from Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs rename to Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs index 49d3dbebc7..325d60e289 100644 --- a/Content.Server/Weapon/Ranged/Systems/ChemicalAmmoSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/ChemicalAmmoSystem.cs @@ -1,10 +1,10 @@ using System.Linq; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Weapon.Ranged.Components; +using Content.Server.Weapons.Ranged.Components; using Content.Shared.Chemistry.Components; using Content.Shared.Weapons.Ranged.Events; -namespace Content.Server.Weapon.Ranged.Systems +namespace Content.Server.Weapons.Ranged.Systems { public sealed class ChemicalAmmoSystem : EntitySystem { diff --git a/Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs b/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs similarity index 69% rename from Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs rename to Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs index 993322a52d..f9986767fe 100644 --- a/Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/FlyBySoundSystem.cs @@ -1,5 +1,5 @@ using Content.Shared.Weapons.Ranged.Systems; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {} diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs similarity index 94% rename from Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs rename to Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index cbfe3c22c7..f6eaacd476 100644 --- a/Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -1,7 +1,7 @@ using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Map; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs similarity index 98% rename from Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs rename to Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs index 7767cf40f0..c5383c4f0e 100644 --- a/Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs @@ -7,7 +7,7 @@ using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Prototypes; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Cartridges.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs similarity index 97% rename from Content.Server/Weapon/Ranged/Systems/GunSystem.Cartridges.cs rename to Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs index 0fc8b5b4d2..15b7347e2a 100644 --- a/Content.Server/Weapon/Ranged/Systems/GunSystem.Cartridges.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs @@ -6,7 +6,7 @@ using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Prototypes; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs similarity index 89% rename from Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs rename to Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs index 7f68d20531..73e815f2c4 100644 --- a/Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs @@ -1,6 +1,6 @@ using Content.Shared.Weapons.Ranged.Components; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { diff --git a/Content.Server/Weapon/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs similarity index 98% rename from Content.Server/Weapon/Ranged/Systems/GunSystem.cs rename to Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 7d451c7491..0fffb94699 100644 --- a/Content.Server/Weapon/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -6,15 +6,12 @@ using Content.Server.Interaction; using Content.Server.Interaction.Components; using Content.Server.Projectiles.Components; using Content.Server.Stunnable; -using Content.Server.Weapon.Melee; -using Content.Server.Weapon.Ranged.Components; -using Content.Shared.Audio; +using Content.Server.Weapons.Melee; +using Content.Server.Weapons.Ranged.Components; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.FixedPoint; -using Content.Shared.StatusEffect; using Content.Shared.Weapons.Melee; -using Content.Shared.Vehicle.Components; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; @@ -28,7 +25,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem : SharedGunSystem { @@ -196,7 +193,7 @@ public sealed partial class GunSystem : SharedGunSystem { if (dmg.Total > FixedPoint2.Zero) { - RaiseNetworkEvent(new DamageEffectEvent(hitEntity), Filter.Pvs(hitEntity, entityManager: EntityManager)); + RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List {result.HitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager)); } PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); diff --git a/Content.Server/Weapon/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs b/Content.Server/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs similarity index 95% rename from Content.Server/Weapon/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs rename to Content.Server/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs index 161bcb7245..0f29aabb65 100644 --- a/Content.Server/Weapon/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs @@ -1,13 +1,12 @@ -using Content.Server.Weapon.Ranged.Components; +using Content.Server.Weapons.Ranged.Components; using Content.Shared.Examine; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Random; -using Robust.Shared.Timing; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed class RechargeBasicEntityAmmoSystem : EntitySystem { diff --git a/Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs b/Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs similarity index 98% rename from Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs rename to Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs index 7195c2de5b..113a58f44c 100644 --- a/Content.Server/Weapon/Ranged/Systems/TetherGunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Server.Ghost.Components; using Content.Shared.Administration; using Content.Shared.Weapons.Ranged.Systems; @@ -14,7 +13,7 @@ using Robust.Shared.Players; using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Content.Server.Weapon.Ranged.Systems; +namespace Content.Server.Weapons.Ranged.Systems; public sealed class TetherGunSystem : SharedTetherGunSystem { diff --git a/Content.Server/Weapons/Ranged/Commands/TetherGunCommand.cs b/Content.Server/Weapons/TetherGunCommand.cs similarity index 89% rename from Content.Server/Weapons/Ranged/Commands/TetherGunCommand.cs rename to Content.Server/Weapons/TetherGunCommand.cs index 436a2224f5..38136ce220 100644 --- a/Content.Server/Weapons/Ranged/Commands/TetherGunCommand.cs +++ b/Content.Server/Weapons/TetherGunCommand.cs @@ -1,10 +1,10 @@ using Content.Server.Administration; -using Content.Server.Weapon.Ranged.Systems; +using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Administration; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Console; -namespace Content.Server.Weapons.Ranged.Commands; +namespace Content.Server.Weapons; [AdminCommand(AdminFlags.Fun)] public sealed class TetherGunCommand : IConsoleCommand diff --git a/Content.Server/Wieldable/WieldableSystem.cs b/Content.Server/Wieldable/WieldableSystem.cs index fa4131a139..4b4458ebce 100644 --- a/Content.Server/Wieldable/WieldableSystem.cs +++ b/Content.Server/Wieldable/WieldableSystem.cs @@ -1,7 +1,6 @@ using Content.Server.DoAfter; using Content.Server.Hands.Components; using Content.Server.Hands.Systems; -using Content.Server.Weapon.Melee; using Content.Server.Wieldable.Components; using Content.Shared.Hands; using Content.Shared.Hands.Components; @@ -12,6 +11,7 @@ using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Player; using Content.Server.Actions.Events; +using Content.Server.Weapons.Melee.Events; namespace Content.Server.Wieldable diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactHeatTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactHeatTriggerSystem.cs index ff486deade..6fd85cd366 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactHeatTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactHeatTriggerSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Interaction; using Content.Shared.Temperature; using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; using Robust.Server.GameObjects; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactInteractionTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactInteractionTriggerSystem.cs index c510446bed..79dd85ff2b 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactInteractionTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactInteractionTriggerSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Interaction; using Content.Shared.Physics.Pull; using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 22f591bdcd..8e0cfe83a9 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -3,7 +3,6 @@ using Robust.Shared.Random; using Content.Server.Body.Systems; using Content.Server.Disease.Components; using Content.Server.Drone.Components; -using Content.Server.Weapon.Melee; using Content.Shared.Chemistry.Components; using Content.Shared.MobState.Components; using Content.Server.Disease; @@ -13,6 +12,8 @@ using Content.Server.Inventory; using Robust.Shared.Prototypes; using Content.Server.Speech; using Content.Server.Chat.Systems; +using Content.Server.Weapons.Melee.Events; +using Content.Shared.Movement.Systems; using Content.Shared.Damage; using Content.Shared.Zombies; diff --git a/Content.Server/Zombies/ZombifyOnDeathSystem.cs b/Content.Server/Zombies/ZombifyOnDeathSystem.cs index be3d437b8b..3548b0506c 100644 --- a/Content.Server/Zombies/ZombifyOnDeathSystem.cs +++ b/Content.Server/Zombies/ZombifyOnDeathSystem.cs @@ -16,7 +16,6 @@ using Content.Server.Ghost.Roles.Components; using Content.Server.Hands.Components; using Content.Server.Mind.Commands; using Content.Server.Temperature.Components; -using Content.Server.Weapon.Melee.Components; using Content.Shared.Movement.Components; using Content.Shared.MobState; using Robust.Shared.Prototypes; @@ -29,6 +28,7 @@ using Content.Server.Humanoid; using Content.Server.IdentityManagement; using Content.Shared.Humanoid; using Content.Shared.Movement.Systems; +using Content.Shared.Weapons.Melee; using Robust.Shared.Audio; namespace Content.Server.Zombies @@ -119,8 +119,7 @@ namespace Content.Server.Zombies //This is the actual damage of the zombie. We assign the visual appearance //and range here because of stuff we'll find out later var melee = EnsureComp(target); - melee.Arc = zombiecomp.AttackArc; - melee.ClickArc = zombiecomp.AttackArc; + melee.Animation = zombiecomp.AttackAnimation; melee.Range = 0.75f; //We have specific stuff for humanoid zombies because they matter more diff --git a/Content.Shared/CombatMode/CombatModeSystemMessages.cs b/Content.Shared/CombatMode/Events/CombatModeSystemMessages.cs similarity index 62% rename from Content.Shared/CombatMode/CombatModeSystemMessages.cs rename to Content.Shared/CombatMode/Events/CombatModeSystemMessages.cs index 1a728d7f88..819a449349 100644 --- a/Content.Shared/CombatMode/CombatModeSystemMessages.cs +++ b/Content.Shared/CombatMode/Events/CombatModeSystemMessages.cs @@ -15,16 +15,5 @@ namespace Content.Shared.CombatMode public TargetingZone TargetZone { get; } } - - [Serializable, NetSerializable] - public sealed class SetCombatModeActiveMessage : EntityEventArgs - { - public SetCombatModeActiveMessage(bool active) - { - Active = active; - } - - public bool Active { get; } - } } } diff --git a/Content.Shared/CombatMode/Events/TogglePrecisionModeEvent.cs b/Content.Shared/CombatMode/Events/TogglePrecisionModeEvent.cs new file mode 100644 index 0000000000..e0ac52cea1 --- /dev/null +++ b/Content.Shared/CombatMode/Events/TogglePrecisionModeEvent.cs @@ -0,0 +1,7 @@ +using Content.Shared.Actions; + +namespace Content.Shared.CombatMode; + +public sealed class TogglePrecisionModeEvent : InstantActionEvent +{ +} diff --git a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs index d0cab6d096..d55aba6619 100644 --- a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs +++ b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.CombatMode; using Content.Shared.Actions; namespace Content.Shared.CombatMode.Pacification @@ -18,11 +17,9 @@ namespace Content.Shared.CombatMode.Pacification if (!TryComp(uid, out var combatMode)) return; - if (combatMode.DisarmAction != null) - { - _actionsSystem.SetToggled(combatMode.DisarmAction, false); - _actionsSystem.SetEnabled(combatMode.DisarmAction, false); - } + if (combatMode.CanDisarm != null) + combatMode.CanDisarm = false; + if (combatMode.CombatToggleAction != null) { combatMode.IsInCombatMode = false; @@ -35,8 +32,8 @@ namespace Content.Shared.CombatMode.Pacification if (!TryComp(uid, out var combatMode)) return; - if (combatMode.DisarmAction != null) - _actionsSystem.SetEnabled(combatMode.DisarmAction, true); + if (combatMode.CanDisarm != null) + combatMode.CanDisarm = true; if (combatMode.CombatToggleAction != null) _actionsSystem.SetEnabled(combatMode.CombatToggleAction, true); } diff --git a/Content.Shared/CombatMode/SharedCombatModeComponent.cs b/Content.Shared/CombatMode/SharedCombatModeComponent.cs index 88fac66fec..a68560b055 100644 --- a/Content.Shared/CombatMode/SharedCombatModeComponent.cs +++ b/Content.Shared/CombatMode/SharedCombatModeComponent.cs @@ -5,17 +5,21 @@ using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; namespace Content.Shared.CombatMode { [NetworkedComponent()] public abstract class SharedCombatModeComponent : Component { - private bool _isInCombatMode; - private TargetingZone _activeZone; + #region Disarm - [DataField("disarmFailChance")] - public readonly float BaseDisarmFailChance = 0.75f; + /// + /// Whether we are able to disarm. This requires our active hand to be free. + /// False if it's toggled off for whatever reason, null if it's not possible. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("disarm")] + public bool? CanDisarm; [DataField("disarmFailSound")] public readonly SoundSpecifier DisarmFailSound = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg"); @@ -23,14 +27,13 @@ namespace Content.Shared.CombatMode [DataField("disarmSuccessSound")] public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); - [DataField("disarmActionId", customTypeSerializer:typeof(PrototypeIdSerializer))] - public readonly string DisarmActionId = "Disarm"; + [DataField("disarmFailChance")] + public readonly float BaseDisarmFailChance = 0.75f; - [DataField("canDisarm")] - public bool CanDisarm; + #endregion - [DataField("disarmAction")] // must be a data-field to properly save cooldown when saving game state. - public EntityTargetAction? DisarmAction; + private bool _isInCombatMode; + private TargetingZone _activeZone; [DataField("combatToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer))] public readonly string CombatToggleActionId = "CombatModeToggle"; @@ -49,19 +52,6 @@ namespace Content.Shared.CombatMode if (CombatToggleAction != null) EntitySystem.Get().SetToggled(CombatToggleAction, _isInCombatMode); Dirty(); - - // Regenerate physics contacts -> Can probably just selectively check - /* Still a bit jank so left disabled for now. - if (Owner.TryGetComponent(out PhysicsComponent? physicsComponent)) - { - if (value) - { - physicsComponent.WakeBody(); - } - - physicsComponent.RegenerateContacts(); - } - */ } } @@ -76,35 +66,5 @@ namespace Content.Shared.CombatMode Dirty(); } } - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - - if (curState is not CombatModeComponentState state) - return; - - IsInCombatMode = state.IsInCombatMode; - ActiveZone = state.TargetingZone; - } - - - public override ComponentState GetComponentState() - { - return new CombatModeComponentState(IsInCombatMode, ActiveZone); - } - - [Serializable, NetSerializable] - protected sealed class CombatModeComponentState : ComponentState - { - public bool IsInCombatMode { get; } - public TargetingZone TargetingZone { get; } - - public CombatModeComponentState(bool isInCombatMode, TargetingZone targetingZone) - { - IsInCombatMode = isInCombatMode; - TargetingZone = targetingZone; - } - } } } diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs index d08544ad47..c5c5b3d880 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -1,21 +1,20 @@ using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; +using Content.Shared.Targeting; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.CombatMode { public abstract class SharedCombatModeSystem : EntitySystem { - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeNetworkEvent(CombatModeActiveHandler); - SubscribeLocalEvent(CombatModeActiveHandler); - SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnActionPerform); @@ -31,30 +30,17 @@ namespace Content.Shared.CombatMode if (component.CombatToggleAction != null) _actionsSystem.AddAction(uid, component.CombatToggleAction, null); - - if (component.DisarmAction == null - && component.CanDisarm - && _protoMan.TryIndex(component.DisarmActionId, out EntityTargetActionPrototype? disarmProto)) - { - component.DisarmAction = new(disarmProto); - } - - if (component.DisarmAction != null && component.CanDisarm) - _actionsSystem.AddAction(uid, component.DisarmAction, null); } private void OnShutdown(EntityUid uid, SharedCombatModeComponent component, ComponentShutdown args) { if (component.CombatToggleAction != null) _actionsSystem.RemoveAction(uid, component.CombatToggleAction); - - if (component.DisarmAction != null) - _actionsSystem.RemoveAction(uid, component.DisarmAction); } - public bool IsInCombatMode(EntityUid entity) + public bool IsInCombatMode(EntityUid? entity, SharedCombatModeComponent? component = null) { - return TryComp(entity, out var combatMode) && combatMode.IsInCombatMode; + return entity != null && Resolve(entity.Value, ref component, false) && component.IsInCombatMode; } private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args) @@ -66,17 +52,19 @@ namespace Content.Shared.CombatMode args.Handled = true; } - private void CombatModeActiveHandler(CombatModeSystemMessages.SetCombatModeActiveMessage ev, EntitySessionEventArgs eventArgs) + [Serializable, NetSerializable] + protected sealed class CombatModeComponentState : ComponentState { - var entity = eventArgs.SenderSession.AttachedEntity; + public bool IsInCombatMode { get; } + public TargetingZone TargetingZone { get; } - if (entity == null || !EntityManager.TryGetComponent(entity, out SharedCombatModeComponent? combatModeComponent)) - return; - - combatModeComponent.IsInCombatMode = ev.Active; + public CombatModeComponentState(bool isInCombatMode, TargetingZone targetingZone) + { + IsInCombatMode = isInCombatMode; + TargetingZone = targetingZone; + } } } public sealed class ToggleCombatActionEvent : InstantActionEvent { } - public sealed class DisarmActionEvent : EntityTargetActionEvent { } } diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 83432df8b2..c341b8d359 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -22,7 +22,6 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward"; public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward"; public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu"; - public static readonly BoundKeyFunction OpenContextMenu = "OpenContextMenu"; public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu"; public static readonly BoundKeyFunction SmartEquipBackpack = "SmartEquipBackpack"; diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 06b6be2bae..b9f723966d 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -198,12 +198,9 @@ namespace Content.Shared.Interaction if (target != null && Deleted(target.Value)) return; - // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? - if (!altInteract && TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode) + if (TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode) { - // Wide attack if there isn't a target or the target is out of range, click attack otherwise. - var shouldWideAttack = target == null || !InRangeUnobstructed(user, target.Value); - DoAttack(user, coordinates, shouldWideAttack, target); + // Eat the input return; } @@ -300,12 +297,6 @@ namespace Content.Shared.Interaction checkAccess: false); } - public virtual void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, - EntityUid? targetUid = null) - { - // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. - } - public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs index 8365af4594..d779e8028e 100644 --- a/Content.Shared/Item/SharedItemSystem.cs +++ b/Content.Shared/Item/SharedItemSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.CombatMode; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Inventory.Events; @@ -9,8 +10,9 @@ namespace Content.Shared.Item; public abstract class SharedItemSystem : EntitySystem { - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] protected readonly SharedContainerSystem Container = default!; public override void Initialize() { @@ -69,7 +71,7 @@ public abstract class SharedItemSystem : EntitySystem private void OnHandInteract(EntityUid uid, ItemComponent component, InteractHandEvent args) { - if (args.Handled) + if (args.Handled || _combatMode.IsInCombatMode(args.User)) return; args.Handled = _handsSystem.TryPickup(args.User, uid, animateUser: false); @@ -121,8 +123,8 @@ public abstract class SharedItemSystem : EntitySystem // if the item already in a container (that is not the same as the user's), then change the text. // this occurs when the item is in their inventory or in an open backpack - _container.TryGetContainingContainer(args.User, out var userContainer); - if (_container.TryGetContainingContainer(args.Target, out var container) && container != userContainer) + Container.TryGetContainingContainer(args.User, out var userContainer); + if (Container.TryGetContainingContainer(args.Target, out var container) && container != userContainer) verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory"); else verb.Text = Loc.GetString("pick-up-verb-get-data-text"); diff --git a/Content.Shared/Throwing/ThrowingSystem.cs b/Content.Shared/Throwing/ThrowingSystem.cs index 661aa383d1..8a02f0efc8 100644 --- a/Content.Shared/Throwing/ThrowingSystem.cs +++ b/Content.Shared/Throwing/ThrowingSystem.cs @@ -29,7 +29,6 @@ public sealed class ThrowingSystem : EntitySystem /// The entity being thrown. /// A vector pointing from the entity to its destination. /// How much the direction vector should be multiplied for velocity. - /// /// The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced public void TryThrow( EntityUid uid, diff --git a/Content.Shared/Weapons/Melee/AttackEvent.cs b/Content.Shared/Weapons/Melee/AttackEvent.cs deleted file mode 100644 index 06f238a625..0000000000 --- a/Content.Shared/Weapons/Melee/AttackEvent.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Robust.Shared.Map; - -namespace Content.Shared.Weapons.Melee -{ - /// - /// Raised directed on the used entity when a target entity is click attacked by a user. - /// - public sealed class ClickAttackEvent : HandledEntityEventArgs - { - /// - /// Entity used to attack, for broadcast purposes. - /// - public EntityUid Used { get; } - - /// - /// Entity that triggered the attack. - /// - public EntityUid User { get; } - - /// - /// The original location that was clicked by the user. - /// - public EntityCoordinates ClickLocation { get; } - - /// - /// The entity that was attacked. - /// - public EntityUid? Target { get; } - - public ClickAttackEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation, EntityUid? target = null) - { - Used = used; - User = user; - ClickLocation = clickLocation; - Target = target; - } - } - - /// - /// Raised directed on the used entity when a target entity is wide attacked by a user. - /// - public sealed class WideAttackEvent : HandledEntityEventArgs - { - /// - /// Entity used to attack, for broadcast purposes. - /// - public EntityUid Used { get; } - - /// - /// Entity that triggered the attack. - /// - public EntityUid User { get; } - - /// - /// The original location that was clicked by the user. - /// - public EntityCoordinates ClickLocation { get; } - - public WideAttackEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation) - { - Used = used; - User = user; - ClickLocation = clickLocation; - } - } - - /// - /// Event raised on entities that have been attacked. - /// - public sealed class AttackedEvent : EntityEventArgs - { - /// - /// Entity used to attack, for broadcast purposes. - /// - public EntityUid Used { get; } - - /// - /// Entity that triggered the attack. - /// - public EntityUid User { get; } - - /// - /// The original location that was clicked by the user. - /// - public EntityCoordinates ClickLocation { get; } - - public AttackedEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation) - { - Used = used; - User = user; - ClickLocation = clickLocation; - } - } -} diff --git a/Content.Shared/Weapons/Melee/DamageEffectEvent.cs b/Content.Shared/Weapons/Melee/DamageEffectEvent.cs index bf01ef61b7..ed5eb15a02 100644 --- a/Content.Shared/Weapons/Melee/DamageEffectEvent.cs +++ b/Content.Shared/Weapons/Melee/DamageEffectEvent.cs @@ -8,10 +8,16 @@ namespace Content.Shared.Weapons.Melee; [Serializable, NetSerializable] public sealed class DamageEffectEvent : EntityEventArgs { - public EntityUid Entity; + /// + /// Color to play for the damage flash. + /// + public Color Color; - public DamageEffectEvent(EntityUid entity) + public List Entities; + + public DamageEffectEvent(Color color, List entities) { - Entity = entity; + Color = color; + Entities = entities; } } diff --git a/Content.Shared/Weapons/Melee/Events/AttackEvent.cs b/Content.Shared/Weapons/Melee/Events/AttackEvent.cs new file mode 100644 index 0000000000..fdee5d507b --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/AttackEvent.cs @@ -0,0 +1,47 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events +{ + [Serializable, NetSerializable] + public abstract class AttackEvent : EntityEventArgs + { + /// + /// Coordinates being attacked. + /// + public readonly EntityCoordinates Coordinates; + + protected AttackEvent(EntityCoordinates coordinates) + { + Coordinates = coordinates; + } + } + + /// + /// Event raised on entities that have been attacked. + /// + public sealed class AttackedEvent : EntityEventArgs + { + /// + /// Entity used to attack, for broadcast purposes. + /// + public EntityUid Used { get; } + + /// + /// Entity that triggered the attack. + /// + public EntityUid User { get; } + + /// + /// The original location that was clicked by the user. + /// + public EntityCoordinates ClickLocation { get; } + + public AttackedEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation) + { + Used = used; + User = user; + ClickLocation = clickLocation; + } + } +} diff --git a/Content.Shared/Weapons/Melee/Events/DisarmAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/DisarmAttackEvent.cs new file mode 100644 index 0000000000..51b6f90904 --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/DisarmAttackEvent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +[Serializable, NetSerializable] +public sealed class DisarmAttackEvent : AttackEvent +{ + public EntityUid? Target; + + public DisarmAttackEvent(EntityUid? target, EntityCoordinates coordinates) : base(coordinates) + { + Target = target; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs new file mode 100644 index 0000000000..e5dadc7aa7 --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/HeavyAttackEvent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +/// +/// Raised on the client when it attempts a heavy attack. +/// +[Serializable, NetSerializable] +public sealed class HeavyAttackEvent : AttackEvent +{ + public readonly EntityUid Weapon; + + public HeavyAttackEvent(EntityUid weapon, EntityCoordinates coordinates) : base(coordinates) + { + Weapon = weapon; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/LightAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/LightAttackEvent.cs new file mode 100644 index 0000000000..9b0f979f7d --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/LightAttackEvent.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +/// +/// Raised when a light attack is made. +/// +[Serializable, NetSerializable] +public sealed class LightAttackEvent : AttackEvent +{ + public readonly EntityUid? Target; + public readonly EntityUid Weapon; + + public LightAttackEvent(EntityUid? target, EntityUid weapon, EntityCoordinates coordinates) : base(coordinates) + { + Target = target; + Weapon = weapon; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/MeleeLungeEvent.cs b/Content.Shared/Weapons/Melee/Events/MeleeLungeEvent.cs new file mode 100644 index 0000000000..22f6ff8987 --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/MeleeLungeEvent.cs @@ -0,0 +1,35 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +/// +/// Data for melee lunges from attacks. +/// +[Serializable, NetSerializable] +public sealed class MeleeLungeEvent : EntityEventArgs +{ + public EntityUid Entity; + + /// + /// Width of the attack angle. + /// + public Angle Angle; + + /// + /// The relative local position to the + /// + public Vector2 LocalPos; + + /// + /// Entity to spawn for the animation + /// + public string? Animation; + + public MeleeLungeEvent(EntityUid uid, Angle angle, Vector2 localPos, string? animation) + { + Entity = uid; + Angle = angle; + LocalPos = localPos; + Animation = animation; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/StartHeavyAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/StartHeavyAttackEvent.cs new file mode 100644 index 0000000000..d7f220e75e --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/StartHeavyAttackEvent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +[Serializable, NetSerializable] +public sealed class StartHeavyAttackEvent : EntityEventArgs +{ + public readonly EntityUid Weapon; + + public StartHeavyAttackEvent(EntityUid weapon) + { + Weapon = weapon; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/StopAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/StopAttackEvent.cs new file mode 100644 index 0000000000..e7fa5cd4a2 --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/StopAttackEvent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +[Serializable, NetSerializable] +public sealed class StopAttackEvent : EntityEventArgs +{ + public readonly EntityUid Weapon; + + public StopAttackEvent(EntityUid weapon) + { + Weapon = weapon; + } +} diff --git a/Content.Shared/Weapons/Melee/Events/StopHeavyAttackEvent.cs b/Content.Shared/Weapons/Melee/Events/StopHeavyAttackEvent.cs new file mode 100644 index 0000000000..75d2ff0aa5 --- /dev/null +++ b/Content.Shared/Weapons/Melee/Events/StopHeavyAttackEvent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Melee.Events; + +/// +/// Raised by the client if it pre-emptively stops a heavy attack. +/// +[Serializable, NetSerializable] +public sealed class StopHeavyAttackEvent : EntityEventArgs +{ + public readonly EntityUid Weapon; + + public StopHeavyAttackEvent(EntityUid weapon) + { + Weapon = weapon; + } +} diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponAnimationPrototype.cs b/Content.Shared/Weapons/Melee/MeleeWeaponAnimationPrototype.cs deleted file mode 100644 index cdfc369393..0000000000 --- a/Content.Shared/Weapons/Melee/MeleeWeaponAnimationPrototype.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Weapons.Melee -{ - [Prototype("MeleeWeaponAnimation")] - public sealed class MeleeWeaponAnimationPrototype : IPrototype - { - [ViewVariables] - [IdDataFieldAttribute] - public string ID { get; } = default!; - - [ViewVariables] - [DataField("state")] - public string State { get; } = string.Empty; - - [ViewVariables] - [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Prototype { get; } = "WeaponArc"; - - [ViewVariables] - [DataField("length")] - public TimeSpan Length { get; } = TimeSpan.FromSeconds(0.5f); - - [ViewVariables] - [DataField("speed")] - public float Speed { get; } = 1; - - [ViewVariables] - [DataField("color")] - public Vector4 Color { get; } = new(1,1,1,1); - - [ViewVariables] - [DataField("colorDelta")] - public Vector4 ColorDelta { get; } = Vector4.Zero; - - [ViewVariables] - [DataField("arcType")] - public WeaponArcType ArcType { get; } = WeaponArcType.Slash; - - [ViewVariables] - [DataField("width")] - public float Width { get; } = 90; - } - - public enum WeaponArcType - { - Slash, - Poke, - } -} diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs new file mode 100644 index 0000000000..bc9b55d555 --- /dev/null +++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs @@ -0,0 +1,146 @@ +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Weapons.Melee; + +/// +/// When given to a mob lets them do unarmed attacks, or when given to an item lets someone wield it to do attacks. +/// +[RegisterComponent, NetworkedComponent] +public sealed class MeleeWeaponComponent : Component +{ + /// + /// Should the melee weapon's damage stats be examinable. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("hidden")] + public bool HideFromExamine { get; set; } = false; + + /// + /// Next time this component is allowed to light attack. Heavy attacks are wound up and never have a cooldown. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("nextAttack")] + public TimeSpan NextAttack; + + /* + * Melee combat works based around 2 types of attacks: + * 1. Click attacks with left-click. This attacks whatever is under your mnouse + * 2. Wide attacks with right-click + left-click. This attacks whatever is in the direction of your mouse. + */ + + /// + /// How many times we can attack per second. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("attackRate")] + public float AttackRate = 1f; + + /// + /// Are we currently holding down the mouse for an attack. + /// Used so we can't just hold the mouse button and attack constantly. + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool Attacking = false; + + /// + /// When did we start a heavy attack. + /// + /// + [ViewVariables(VVAccess.ReadWrite), DataField("windUpStart")] + public TimeSpan? WindUpStart; + + /// + /// How long it takes a heavy attack to windup. + /// + [ViewVariables] + public TimeSpan WindupTime => AttackRate > 0 ? TimeSpan.FromSeconds(1 / AttackRate * HeavyWindupModifier) : TimeSpan.Zero; + + /// + /// Heavy attack windup time gets multiplied by this value and the light attack cooldown. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("heavyWindupModifier")] + public float HeavyWindupModifier = 1.5f; + + /// + /// Light attacks get multiplied by this over the base value. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("heavyDamageModifier")] + public FixedPoint2 HeavyDamageModifier = FixedPoint2.New(2); + + /// + /// Base damage for this weapon. Can be modified via heavy damage or other means. + /// + [DataField("damage", required:true)] + [ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier Damage = default!; + + [DataField("bluntStaminaDamageFactor")] + [ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 BluntStaminaDamageFactor { get; set; } = 0.5f; + + /// + /// Nearest edge range to hit an entity. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("range")] + public float Range = 1f; + + /// + /// Total width of the angle for wide attacks. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("angle")] + public Angle Angle = Angle.FromDegrees(60); + + [ViewVariables(VVAccess.ReadWrite), DataField("animation", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Animation = "WeaponArcSlash"; + + // Sounds + + /// + /// This gets played whenever a melee attack is done. This is predicted by the client. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("soundSwing")] + public SoundSpecifier SwingSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg") + { + Params = AudioParams.Default.WithVolume(-3f).WithVariation(0.025f), + }; + + // We do not predict the below sounds in case the client thinks but the server disagrees. If this were the case + // then a player may doubt if the target actually took damage or not. + // If overwatch and apex do this then we probably should too. + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("soundHit")] + public SoundSpecifier? HitSound; + + /// + /// Plays if no damage is done to the target entity. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("soundNoDamage")] + public SoundSpecifier NoDamageSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/tap.ogg"); +} + +[Serializable, NetSerializable] +public sealed class MeleeWeaponComponentState : ComponentState +{ + // None of the other data matters for client as they're not predicted. + + public float AttackRate; + public bool Attacking; + public TimeSpan NextAttack; + public TimeSpan? WindUpStart; + + public MeleeWeaponComponentState(float attackRate, bool attacking, TimeSpan nextAttack, TimeSpan? windupStart) + { + AttackRate = attackRate; + Attacking = attacking; + NextAttack = nextAttack; + WindUpStart = windupStart; + } +} diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponSystemMessages.cs b/Content.Shared/Weapons/Melee/MeleeWeaponSystemMessages.cs deleted file mode 100644 index 2b336af046..0000000000 --- a/Content.Shared/Weapons/Melee/MeleeWeaponSystemMessages.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Weapons.Melee -{ - public static class MeleeWeaponSystemMessages - { - [Serializable, NetSerializable] - public sealed class PlayMeleeWeaponAnimationMessage : EntityEventArgs - { - public PlayMeleeWeaponAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, EntityUid source, List hits, bool textureEffect = false, bool arcFollowAttacker = true) - { - ArcPrototype = arcPrototype; - Angle = angle; - Attacker = attacker; - Source = source; - Hits = hits; - TextureEffect = textureEffect; - ArcFollowAttacker = arcFollowAttacker; - } - - public string ArcPrototype { get; } - public Angle Angle { get; } - public EntityUid Attacker { get; } - public EntityUid Source { get; } - public List Hits { get; } - public bool TextureEffect { get; } - public bool ArcFollowAttacker { get; } - } - - [Serializable, NetSerializable] - public sealed class PlayLungeAnimationMessage : EntityEventArgs - { - public Angle Angle { get; } - public EntityUid Source { get; } - - public PlayLungeAnimationMessage(Angle angle, EntityUid source) - { - Angle = angle; - Source = source; - } - } - } -} diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs new file mode 100644 index 0000000000..42da7f0133 --- /dev/null +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -0,0 +1,325 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.CombatMode; +using Content.Shared.Hands.Components; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Weapons.Melee; + +public abstract class SharedMeleeWeaponSystem : EntitySystem +{ + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] protected readonly ActionBlockerSystem Blocker = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; + [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + + protected ISawmill Sawmill = default!; + + /// + /// If an attack is released within this buffer it's assumed to be full damage. + /// + public const float GracePeriod = 0.05f; + + public override void Initialize() + { + base.Initialize(); + Sawmill = Logger.GetSawmill("melee"); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + + SubscribeAllEvent(OnLightAttack); + SubscribeAllEvent(OnStartHeavyAttack); + SubscribeAllEvent(OnStopHeavyAttack); + SubscribeAllEvent(OnHeavyAttack); + SubscribeAllEvent(OnDisarmAttack); + SubscribeAllEvent(OnStopAttack); + } + + private void OnStopAttack(StopAttackEvent msg, EntitySessionEventArgs args) + { + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + var weapon = GetWeapon(user.Value); + + if (weapon?.Owner != msg.Weapon) + return; + + if (!weapon.Attacking) + return; + + weapon.Attacking = false; + Dirty(weapon); + } + + private void OnStartHeavyAttack(StartHeavyAttackEvent msg, EntitySessionEventArgs args) + { + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + var weapon = GetWeapon(user.Value); + + if (weapon?.Owner != msg.Weapon) + return; + + DebugTools.Assert(weapon.WindUpStart == null); + weapon.WindUpStart = Timing.CurTime; + Dirty(weapon); + } + + protected abstract void Popup(string message, EntityUid? uid, EntityUid? user); + + private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args) + { + var user = args.SenderSession.AttachedEntity; + + if (user == null) + return; + + var weapon = GetWeapon(user.Value); + + if (weapon?.Owner != msg.Weapon) + return; + + AttemptAttack(args.SenderSession.AttachedEntity!.Value, weapon, msg); + } + + private void OnStopHeavyAttack(StopHeavyAttackEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity == null || + !TryComp(msg.Weapon, out var weapon)) + { + return; + } + + var userWeapon = GetWeapon(args.SenderSession.AttachedEntity.Value); + + if (userWeapon != weapon) + return; + + if (weapon.WindUpStart.Equals(null)) + { + return; + } + + weapon.WindUpStart = null; + Dirty(weapon); + } + + private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity == null || + !TryComp(msg.Weapon, out var weapon)) + { + return; + } + + var userWeapon = GetWeapon(args.SenderSession.AttachedEntity.Value); + + if (userWeapon != weapon) + return; + + AttemptAttack(args.SenderSession.AttachedEntity.Value, weapon, msg); + } + + private void OnDisarmAttack(DisarmAttackEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity == null) + { + return; + } + + var userWeapon = GetWeapon(args.SenderSession.AttachedEntity.Value); + + if (userWeapon == null) + return; + + AttemptAttack(args.SenderSession.AttachedEntity.Value, userWeapon, msg); + } + + private void OnGetState(EntityUid uid, MeleeWeaponComponent component, ref ComponentGetState args) + { + args.State = new MeleeWeaponComponentState(component.AttackRate, component.Attacking, component.NextAttack, + component.WindUpStart); + } + + private void OnHandleState(EntityUid uid, MeleeWeaponComponent component, ref ComponentHandleState args) + { + if (args.Current is not MeleeWeaponComponentState state) + return; + + component.Attacking = state.Attacking; + component.AttackRate = state.AttackRate; + component.NextAttack = state.NextAttack; + component.WindUpStart = state.WindUpStart; + } + + public MeleeWeaponComponent? GetWeapon(EntityUid entity) + { + MeleeWeaponComponent? melee; + + // Use inhands entity if we got one. + if (EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands) && + hands.ActiveHandEntity is { } held) + { + if (EntityManager.TryGetComponent(held, out melee)) + { + return melee; + } + + return null; + } + + if (TryComp(entity, out melee)) + { + return melee; + } + + return null; + } + + public void AttemptLightAttack(EntityUid user, MeleeWeaponComponent weapon, EntityUid target) + { + if (!TryComp(target, out var targetXform)) + return; + + AttemptAttack(user, weapon, new LightAttackEvent(target, weapon.Owner, targetXform.Coordinates)); + } + + public void AttemptDisarmAttack(EntityUid user, MeleeWeaponComponent weapon, EntityUid target) + { + if (!TryComp(target, out var targetXform)) + return; + + AttemptAttack(user, weapon, new DisarmAttackEvent(target, targetXform.Coordinates)); + } + + /// + /// Called when a windup is finished and an attack is tried. + /// + private void AttemptAttack(EntityUid user, MeleeWeaponComponent weapon, AttackEvent attack) + { + var curTime = Timing.CurTime; + + if (weapon.NextAttack > curTime) + return; + + if (!Blocker.CanAttack(user)) + return; + + // Windup time checked elsewhere. + + if (!CombatMode.IsInCombatMode(user)) + return; + + if (weapon.NextAttack < curTime) + weapon.NextAttack = curTime; + + weapon.NextAttack += TimeSpan.FromSeconds(1f / weapon.AttackRate); + + // Attack confirmed + // Play a sound to give instant feedback; same with playing the animations + Audio.PlayPredicted(weapon.SwingSound, weapon.Owner, user); + + switch (attack) + { + case LightAttackEvent light: + DoLightAttack(user, light, weapon); + break; + case DisarmAttackEvent disarm: + DoDisarm(user, disarm, weapon); + break; + case HeavyAttackEvent heavy: + DoHeavyAttack(user, heavy, weapon); + break; + default: + throw new NotImplementedException(); + } + + DoLungeAnimation(user, weapon.Angle, attack.Coordinates.ToMap(EntityManager), weapon.Animation); + weapon.Attacking = true; + Dirty(weapon); + } + + /// + /// When an attack is released get the actual modifier for damage done. + /// + public float GetModifier(MeleeWeaponComponent component, bool lightAttack) + { + if (lightAttack) + return 1f; + + var windup = component.WindUpStart; + if (windup == null) + return 0f; + + var releaseTime = (Timing.CurTime - windup.Value).TotalSeconds; + var windupTime = component.WindupTime.TotalSeconds; + + // Wraps around back to 0 + releaseTime %= (2 * windupTime); + + var releaseDiff = Math.Abs(releaseTime - windupTime); + + if (releaseDiff < 0) + releaseDiff = Math.Min(0, releaseDiff + GracePeriod); + else + releaseDiff = Math.Max(0, releaseDiff - GracePeriod); + + var fraction = (windupTime - releaseDiff) / windupTime; + + if (fraction < 0.4) + fraction = 0; + + DebugTools.Assert(fraction <= 1); + return (float) fraction * component.HeavyDamageModifier.Float(); + } + + protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, MeleeWeaponComponent component) + { + + } + + protected virtual void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, MeleeWeaponComponent component) + { + + } + + protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component) + { + if (Deleted(ev.Target) || + user == ev.Target) + return false; + + return true; + } + + private void DoLungeAnimation(EntityUid user, Angle angle, MapCoordinates coordinates, string? animation) + { + // TODO: Assert that offset eyes are still okay. + if (!TryComp(user, out var userXform)) + return; + + var invMatrix = userXform.InvWorldMatrix; + var localPos = invMatrix.Transform(coordinates.Position); + + if (localPos.LengthSquared <= 0f) + return; + + localPos = userXform.LocalRotation.RotateVec(localPos); + DoLunge(user, angle, localPos, animation); + } + + public abstract void DoLunge(EntityUid user, Angle angle, Vector2 localPos, string? animation); +} diff --git a/Content.Shared/Zombies/ZombieComponent.cs b/Content.Shared/Zombies/ZombieComponent.cs index 172c293924..e98380531a 100644 --- a/Content.Shared/Zombies/ZombieComponent.cs +++ b/Content.Shared/Zombies/ZombieComponent.cs @@ -1,6 +1,6 @@ using Content.Shared.Roles; -using Content.Shared.Weapons.Melee; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Zombies @@ -52,8 +52,8 @@ namespace Content.Shared.Zombies /// /// The attack arc of the zombie /// - [DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string AttackArc = "claw"; + [DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string AttackAnimation = "WeaponArcClaw"; /// /// The role prototype of the zombie antag role diff --git a/Resources/Locale/en-US/actions/actions/combat-mode.ftl b/Resources/Locale/en-US/actions/actions/combat-mode.ftl index bd15bd1d2f..978038d39d 100644 --- a/Resources/Locale/en-US/actions/actions/combat-mode.ftl +++ b/Resources/Locale/en-US/actions/actions/combat-mode.ftl @@ -1,6 +1,12 @@ action-name-combat = [color=red]Combat Mode[/color] -action-description-combat = Enter combat mode. +action-description-combat = Enter combat mode + +action-popup-combat = Combat mode disabled +action-popup-combat-enabled = Combat mode enabled -action-popup-combat = Combat mode disabled. -action-popup-combat-enabled = Combat mode enabled! +action-name-precision = [color=red]Precision mode[/color] +action-description-precision = Enter precision mode for combat, attacking what is under your cursor. + +action-popup-precision = Precision mode disabled +action-popup-precision-enabled = Precision mode enabled diff --git a/Resources/Locale/en-US/actions/actions/disarm-action.ftl b/Resources/Locale/en-US/actions/actions/disarm-action.ftl index 7dded67bc0..ed60ff86fb 100644 --- a/Resources/Locale/en-US/actions/actions/disarm-action.ftl +++ b/Resources/Locale/en-US/actions/actions/disarm-action.ftl @@ -1,7 +1,6 @@ -disarm-action-free-hand = You need to use a free hand to disarm! - -disarm-action-popup-message-other-clients = {CAPITALIZE(THE($performerName))} fails to disarm {THE($targetName)}! -disarm-action-popup-message-cursor = You fail to disarm {THE($targetName)}! +disarm-action-disarmable = {THE($targetName)} is not disarmable! +disarm-action-popup-message-other-clients = {CAPITALIZE(THE($performerName))} disarmed {THE($targetName)}! +disarm-action-popup-message-cursor = Disarmed {THE($targetName)}! action-name-disarm = [color=red]Disarm[/color] action-description-disarm = Attempt to [color=red]disarm[/color] someone. diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index da17b7a765..e46937a98a 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -91,6 +91,7 @@ ui-options-function-camera-rotate-right = Rotate right ui-options-function-camera-reset = Reset ui-options-function-use = Use +ui-options-function-alt-use = Alt use ui-options-function-wide-attack = Wide attack ui-options-function-activate-item-in-hand = Activate item in hand ui-options-function-alt-activate-item-in-hand = Alternative activate item in hand @@ -176,24 +177,24 @@ ui-options-function-shuttle-brake = Brake ## Network menu ui-options-net-interp-ratio = State buffer size -ui-options-net-interp-ratio-tooltip = Increasing this will generally make the game more resistant - to server->client packet-loss, however in doing so it - effectively adds slightly more latency and requires the +ui-options-net-interp-ratio-tooltip = Increasing this will generally make the game more resistant + to server->client packet-loss, however in doing so it + effectively adds slightly more latency and requires the client to predict more future ticks. ui-options-net-predict-tick-bias = Prediction tick bias -ui-options-net-predict-tick-bias-tooltip = Increasing this will generally make the game more resistant - to client->server packet-loss, however in doing so it - effectively adds slightly more latency and requires the +ui-options-net-predict-tick-bias-tooltip = Increasing this will generally make the game more resistant + to client->server packet-loss, however in doing so it + effectively adds slightly more latency and requires the client to predict more future ticks. ui-options-net-pvs-entry = PVS entity budget -ui-options-net-pvs-entry-tooltip = This limits the rate at which the server will send new - entities to the client. Lowering this can help reduce +ui-options-net-pvs-entry-tooltip = This limits the rate at which the server will send new + entities to the client. Lowering this can help reduce stuttering due to entity spawning, but can lead to pop-in. ui-options-net-pvs-leave = PVS detach rate -ui-options-net-pvs-leave-tooltip = This limits the rate at which the client will remove - out-of-view entities. Lowering this can help reduce - stuttering when walking around, but could occasionally - lead to mispredicts and other issues. \ No newline at end of file +ui-options-net-pvs-leave-tooltip = This limits the rate at which the client will remove + out-of-view entities. Lowering this can help reduce + stuttering when walking around, but could occasionally + lead to mispredicts and other issues. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index ae05a26f7d..8d2e11dc93 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -30,22 +30,6 @@ useDelay: 1 # equip noise spam. event: !type:ToggleClothingEvent -- type: entityTargetAction - id: Disarm - name: action-name-disarm - description: action-description-disarm - icon: Interface/Actions/disarmOff.png - iconOn: Interface/Actions/disarm.png - repeat: true - useDelay: 1.5 - interactOnMiss: true - event: !type:DisarmActionEvent - canTargetSelf: false - whitelist: - components: - - Hands - - StatusEffects - - type: instantAction id: CombatModeToggle name: action-name-combat diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index b1d4981578..6b917d34ef 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -20,9 +20,10 @@ - type: Clothing sprite: Clothing/Eyes/Glasses/gar.rsi - type: MeleeWeapon + attackRate: 1.5 damage: types: - Blunt: 10 + Blunt: 7 - type: entity parent: ClothingEyesBase diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index 9a031534d0..386260db54 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -11,14 +11,16 @@ - type: BoxingGloves - type: StaminaDamageOnHit damage: 8 #Stam damage values seem a bit higher than regular damage because of the decay, etc - knockdownSound: /Audio/Weapons/boxingbell.ogg + # This needs to be moved to boxinggloves + #knockdownSound: /Audio/Weapons/boxingbell.ogg - type: MeleeWeapon + attackRate: 1.5 damage: types: - Blunt: 0.6 - hitSound: + Blunt: 0.4 + soundHit: collection: BoxingHit - arc: fist + animation: WeaponArcFist - type: Fiber fiberMaterial: fibers-leather fiberColor: fibers-red diff --git a/Resources/Prototypes/Entities/Effects/weapon_arc.yml b/Resources/Prototypes/Entities/Effects/weapon_arc.yml index bbb8011e74..254377142b 100644 --- a/Resources/Prototypes/Entities/Effects/weapon_arc.yml +++ b/Resources/Prototypes/Entities/Effects/weapon_arc.yml @@ -1,12 +1,66 @@ - type: entity - id: WeaponArc - save: false + # Just fades out with no movement animation + id: WeaponArcStatic noSpawn: true components: - - type: Sprite - sprite: Effects/arcs.rsi - netsync: false - noRot: false - offset: 0, -0.85 - drawdepth: Effects - - type: MeleeWeaponArcAnimation + - type: Sprite + sprite: Effects/arcs.rsi + state: spear + netsync: false + drawdepth: Effects + - type: EffectVisuals + - type: WeaponArcVisuals + +# TODO: Camera recoil (try it as a shake, i.e. zoom out and then rotate slightly maybe) +# See https://github.com/gasgiant/Camera-Shake + +- type: entity + id: WeaponArcThrust + parent: WeaponArcStatic + noSpawn: true + components: + - type: WeaponArcVisuals + animation: Thrust + + # TODO: Hold for 0.1, thrust out n distance, then fade out + +- type: entity + id: WeaponArcSlash + parent: WeaponArcStatic + noSpawn: true + components: + - type: WeaponArcVisuals + animation: Slash + + +- type: entity + id: WeaponArcBite + parent: WeaponArcStatic + noSpawn: true + components: + - type: Sprite + state: bite + +- type: entity + id: WeaponArcClaw + parent: WeaponArcStatic + noSpawn: true + components: + - type: Sprite + state: claw + +- type: entity + id: WeaponArcDisarm + parent: WeaponArcStatic + noSpawn: true + components: + - type: Sprite + state: disarm + +- type: entity + id: WeaponArcFist + parent: WeaponArcStatic + noSpawn: true + components: + - type: Sprite + state: fist diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 52a50ea13d..913be8a1bb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -56,8 +56,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: types: Piercing: 5 @@ -133,8 +133,8 @@ - type: MeleeWeapon hidden: true range: 0.3 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: types: Poison: 2 @@ -640,7 +640,7 @@ description: A large marsupial herbivore. It has powerful hind legs and... boxing gloves? components: - type: CombatMode - canDisarm: true + disarm: null - type: Sprite drawdepth: Mobs layers: @@ -659,8 +659,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: claw + angle: 0 + animation: WeaponArcClaw damage: types: Blunt: 10 @@ -679,7 +679,7 @@ description: New church of neo-darwinists actually believe that EVERY animal evolved from a monkey. Tastes like pork, and killing them is both fun and relaxing. components: - type: CombatMode - canDisarm: true + disarm: null - type: NameIdentifier group: Monkey - type: SentienceTarget @@ -1187,8 +1187,8 @@ - type: MeleeWeapon hidden: true range: 0.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: groups: Brute: 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml b/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml index f6ee14d2df..71693c46ac 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/bear.yml @@ -59,8 +59,8 @@ - type: MeleeWeapon hidden: true range: 0.5 - arcwidth: 0 - arc: claw + angle: 0 + animation: WeaponArcClaw damage: groups: Brute: 15 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 5dc8abf93c..e0beed09a7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -57,9 +57,9 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite - hitSound: + angle: 0 + animation: WeaponArcBite + soundHit: path: /Audio/Effects/bite.ogg damage: types: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index e95dfd945c..51d484a00f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -38,8 +38,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: fist + angle: 0 + animation: WeaponArcFist damage: types: Blunt: 20 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 0c9dd8d7da..032bdb0c0c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -77,8 +77,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: types: Piercing: 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 427154eeab..1569968f87 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -43,8 +43,8 @@ - type: MeleeWeapon hidden: true range: 1 - arcwidth: 0 - arc: claw + angle: 0 + animation: WeaponArcClaw damage: types: Slash: 12 @@ -135,11 +135,12 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: fist + angle: 0 + attackRate: 0.75 + animation: WeaponArcFist damage: types: - Blunt: 50 #oof ouch owie my bones + Blunt: 66 #oof ouch owie my bones - type: Fixtures fixtures: - shape: @@ -202,8 +203,8 @@ - type: MeleeWeapon hidden: true range: 1 - arcwidth: 0 - arc: claw + angle: 0 + animation: WeaponArcClaw damage: types: Slash: 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 86b9e0d77d..18162e0a2e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -118,8 +118,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: groups: Brute: 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 8b64810700..eb0b5e9c2f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -56,8 +56,8 @@ - type: MeleeWeapon hidden: true range: 0.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: groups: Brute: 2 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index bb30faab23..0371725abe 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -9,7 +9,7 @@ - type: DiseaseProtection protection: 1 - type: CombatMode - canDisarm: true + disarm: null - type: InputMover - type: MobMover - type: HTN @@ -63,10 +63,10 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - hitSound: + angle: 0 + soundHit: collection: AlienClaw - arc: + animation: WeaponArcClaw damage: groups: Brute: 20 @@ -195,7 +195,6 @@ damage: groups: Brute: 40 - cooldownTime: 2 - type: SlowOnDamage speedModifierThresholds: 1000: 0.7 @@ -234,7 +233,6 @@ damage: groups: Brute: 35 - cooldownTime: 1.5 - type: SlowOnDamage speedModifierThresholds: 450: 0.7 @@ -273,7 +271,6 @@ damage: groups: Brute: 15 - cooldownTime: 0.5 - type: SlowOnDamage speedModifierThresholds: 200: 0.7 @@ -363,8 +360,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: types: Piercing: 5 diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 58cb92de1e..f616d579fd 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -92,7 +92,7 @@ soundPerceivedByOthers: false # A 75% chance for a loud roar would get old fast. - type: MeleeWeapon hidden: true - hitSound: + soundHit: path: /Audio/Effects/bite.ogg damage: types: diff --git a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml index 94adf12216..e0d1a3a246 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dwarf.yml @@ -6,7 +6,7 @@ description: A miserable pile of secrets. components: - type: CombatMode - canDisarm: true + disarm: null - type: InteractionPopup successChance: 1 interactSuccessString: hugging-success-generic diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index eec08c43ca..87a93c6e0f 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -42,8 +42,8 @@ - type: MeleeWeapon hidden: true range: 1.5 - arcwidth: 0 - arc: bite + angle: 0 + animation: WeaponArcBite damage: types: Piercing: 8 diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index 6d8f15758d..3a44293a8b 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -81,13 +81,12 @@ - type: MeleeWeapon hidden: true range: 2 - arcwidth: 30 - arc: fist - cooldownTime: 0.7 - arcCooldownTime: 0.7 + angle: 30 + animation: WeaponArcFist + attackRate: 1.5 damage: types: - Blunt: 22 + Blunt: 20 - type: Actions - type: Guardian - type: InteractionPopup diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 3a115b4bde..8cc3a1200f 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -5,7 +5,7 @@ id: MobHuman components: - type: CombatMode - canDisarm: true + disarm: null - type: InteractionPopup successChance: 1 interactSuccessString: hugging-success-generic diff --git a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml index 9c5708be34..851a800252 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/reptilian.yml @@ -5,7 +5,7 @@ id: MobReptilian components: - type: CombatMode - canDisarm: true + disarm: null - type: InteractionPopup successChance: 1 interactSuccessString: hugging-success-generic diff --git a/Resources/Prototypes/Entities/Mobs/Player/slime.yml b/Resources/Prototypes/Entities/Mobs/Player/slime.yml index 175bfe94fe..c379eeb95e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/slime.yml @@ -4,7 +4,7 @@ id: MobSlimePerson components: - type: CombatMode - canDisarm: true + disarm: null - type: InteractionPopup successChance: 1 interactSuccessString: hugging-success-generic diff --git a/Resources/Prototypes/Entities/Mobs/Player/vox.yml b/Resources/Prototypes/Entities/Mobs/Player/vox.yml index 75cc57cdca..6b25447877 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/vox.yml @@ -5,7 +5,7 @@ id: MobVox components: - type: CombatMode - canDisarm: true + disarm: null - type: InteractionPopup successChance: 1 interactSuccessString: hugging-success-generic diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 7ea89cf01e..0a79e844cf 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -233,6 +233,7 @@ fireStackAlternateState: 3 - type: EnsnareableVisualizer - type: CombatMode + disarm: true - type: Climbing - type: Cuffable - type: Ensnareable @@ -243,11 +244,12 @@ - type: Buckle - type: MeleeWeapon hidden: true - hitSound: + soundHit: collection: Punch - range: 0.8 - arcwidth: 30 - arc: fist + range: 1.5 + angle: 30 + animation: WeaponArcFist + attackRate: 1 damage: types: Blunt: 5 diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index a91f4d406d..c3cc0f14c2 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -25,11 +25,11 @@ damageModifierSet: Scale - type: MeleeWeapon hidden: true - hitSound: + soundHit: path: /Audio/Weapons/pierce.ogg range: 0.8 - arcwidth: 30 - arc: fist + angle: 30 + animation: WeaponArcFist damage: types: Piercing: 5 diff --git a/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml b/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml index 094273183f..891320d372 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml @@ -31,7 +31,7 @@ tags: - Payload # yes, you can make re-usable prank grenades - type: MeleeWeapon - hitSound: + soundHit: collection: BikeHorn params: variation: 0.125 diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 0cb30dd2d3..9f1b4a0ca1 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -21,7 +21,7 @@ - type: UseDelay delay: 1.0 - type: MeleeWeapon - hitSound: + soundHit: collection: ToySqueak damage: types: @@ -105,7 +105,7 @@ sound: path: /Audio/Items/Toys/weh.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Items/Toys/weh.ogg - type: entity @@ -126,7 +126,7 @@ sound: path: /Audio/Items/Toys/muffled_weh.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Items/Toys/muffled_weh.ogg - type: entity @@ -168,7 +168,7 @@ sound: path: /Audio/Effects/bite.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Effects/bite.ogg - type: entity @@ -192,7 +192,7 @@ sound: path: /Audio/Items/Toys/rattle.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Items/Toys/rattle.ogg - type: entity @@ -207,7 +207,7 @@ sound: path: /Audio/Items/Toys/mousesqueek.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Items/Toys/mousesqueek.ogg - type: entity @@ -244,7 +244,7 @@ sound: path: /Audio/Voice/Vox/shriek1.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Voice/Vox/shriek1.ogg ## Figurines @@ -313,7 +313,7 @@ sound: path: /Audio/Items/Toys/ian.ogg - type: MeleeWeapon - hitSound: + soundHit: path: /Audio/Items/Toys/ian.ogg - type: entity @@ -549,9 +549,10 @@ sprite: Objects/Fun/toys.rsi state: foamblade - type: MeleeWeapon + attackRate: 1.5 range: 2.0 - arcwidth: 0 - arc: spear + angle: 0 + animation: WeaponArcThrust damage: types: Blunt: 0 diff --git a/Resources/Prototypes/Entities/Objects/Materials/shards.yml b/Resources/Prototypes/Entities/Objects/Materials/shards.yml index 719f4d9193..f534e3386e 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/shards.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/shards.yml @@ -20,9 +20,10 @@ shard3: "" - type: ItemCooldown - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 5 + Slash: 3.5 - type: Item sprite: Objects/Materials/Shards/shard.rsi - type: CollisionWake diff --git a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml index 93913e51ba..b503f63be1 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml @@ -6,10 +6,11 @@ components: - type: ItemCooldown - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 7 - hitSound: + Slash: 5 + soundHit: path: /Audio/Weapons/bladeslice.ogg - type: Sprite sprite: Objects/Consumable/TrashDrinks/broken_bottle.rsi diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml index 69b8c84402..2805c8f581 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml @@ -41,7 +41,7 @@ damage: types: Blunt: 10 - hitSound: + soundHit: path: /Audio/Weapons/smash.ogg - type: Appearance - type: GenericVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml index 98780d7489..053ef0a325 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml @@ -43,7 +43,7 @@ damage: types: Piercing: 10 - hitSound: + soundHit: path: /Audio/Items/drill_hit.ogg # Scalpel @@ -64,10 +64,11 @@ sprite: Objects/Specific/Medical/Surgery/scalpel.rsi - type: ItemCooldown - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 12 - hitSound: + Slash: 8 + soundHit: path: /Audio/Weapons/bladeslice.ogg - type: entity @@ -173,7 +174,7 @@ damage: groups: Brute: 10 - hitSound: + soundHit: path: /Audio/Weapons/bladeslice.ogg - type: Tool qualities: @@ -194,7 +195,7 @@ damage: groups: Brute: 15 - hitSound: + soundHit: path: /Audio/Items/drill_hit.ogg - type: Tool qualities: @@ -212,10 +213,11 @@ - type: Item heldPrefix: advanced - type: MeleeWeapon + attackRate: 1.5 damage: groups: - Brute: 20 - hitSound: + Brute: 10 + soundHit: path: /Audio/Items/drill_hit.ogg - type: Tool qualities: diff --git a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml index 56511bb769..9e5356e572 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml @@ -17,7 +17,7 @@ damage: types: Blunt: 10 - hitSound: + soundHit: path: "/Audio/Weapons/smash.ogg" - type: Tag tags: diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index bc50916b7e..75c0669c91 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -61,9 +61,10 @@ sprite: Objects/Tools/screwdriver.rsi - type: ItemCooldown - type: MeleeWeapon + attackRate: 1.5 damage: types: - Piercing: 10 + Piercing: 7 - type: Tool qualities: - Screwing @@ -96,9 +97,10 @@ sprite: Objects/Tools/wrench.rsi - type: ItemCooldown - type: MeleeWeapon + attackRate: 1.5 damage: types: - Blunt: 10 + Blunt: 6.5 - type: Tool qualities: - Anchoring diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index b54771ec43..cb6f862745 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -9,10 +9,11 @@ sprite: Objects/Weapons/Melee/armblade.rsi state: icon - type: MeleeWeapon + attackRate: 0.75 damage: types: - Slash: 20 - Piercing: 10 + Slash: 25 + Piercing: 15 - type: Item size: 15 sprite: Objects/Weapons/Melee/armblade.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index 9025bc1a1f..e8941186cc 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -7,9 +7,9 @@ - type: EnergySword litDamageBonus: types: - Slash: 12.5 - Heat: 12.5 - Blunt: -7 + Slash: 8.5 + Heat: 8.5 + Blunt: -4.5 litDisarmMalus: 0.6 - type: Sprite sprite: Objects/Weapons/Melee/e_sword.rsi @@ -21,11 +21,12 @@ shader: unshaded map: [ "blade" ] - type: MeleeWeapon - hitSound: + attackRate: 1.5 + soundHit: path: /Audio/Weapons/genhit1.ogg damage: types: - Blunt: 7 + Blunt: 4.5 - type: Item size: 5 sprite: Objects/Weapons/Melee/e_sword.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml index 2fe49d5513..315ce9bc57 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml @@ -12,19 +12,20 @@ sprite: Objects/Weapons/Melee/fireaxe.rsi state: icon - type: MeleeWeapon + attackRate: 0.75 damage: types: # axes are kinda like sharp hammers, you know? - Blunt: 4 - Slash: 10 - Structural: 5 + Blunt: 5 + Slash: 13 + Structural: 7 - type: Wieldable - type: IncreaseDamageOnWield damage: types: - Blunt: 2 - Slash: 8 - Structural: 45 + Blunt: 2.5 + Slash: 10.5 + Structural: 60 - type: Item size: 150 - type: Clothing diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml index 05930de1eb..f1a9d29f03 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml @@ -15,7 +15,7 @@ damage: types: Slash: 12 - hitSound: + soundHit: path: /Audio/Weapons/bladeslice.ogg - type: Sprite netsync: false @@ -57,9 +57,10 @@ size: 4 state: butch - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 15 + Slash: 10 - type: Item size: 10 sprite: Objects/Weapons/Melee/cleaver.rsi @@ -79,9 +80,10 @@ size: 2 state: icon - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 15 + Slash: 10 - type: Item size: 10 sprite: Objects/Weapons/Melee/combat_knife.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml index e0981849a4..ee97f944f7 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml @@ -20,7 +20,6 @@ types: Piercing: 10 Blunt: 4 - arcCooldownTime: 3 - type: Item size: 24 sprite: Objects/Weapons/Melee/pickaxe.rsi @@ -48,4 +47,3 @@ types: Piercing: 10 Blunt: 4 - arcCooldownTime: 3 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml index 3b6e7b46c3..e8c78fd68a 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/spear.yml @@ -10,14 +10,15 @@ - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/spear.rsi + netsync: false state: spear - type: MeleeWeapon damage: types: Piercing: 10 range: 1.5 - arcwidth: 0 - arc: spear + angle: 0 + animation: WeaponArcThrust - type: DamageOtherOnHit damage: types: @@ -76,10 +77,3 @@ damage: types: Blunt: 5 - -- type: MeleeWeaponAnimation - id: spear - state: spear - length: 0.10 - speed: 6 - arcType: Poke diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 50ca099064..dd70ca9b63 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -9,9 +9,10 @@ sprite: Objects/Weapons/Melee/captain_sabre.rsi state: icon - type: MeleeWeapon + attackRate: 1.5 damage: types: - Slash: 26 #cmon, it has to be at least BETTER than the rest. + Slash: 17 #cmon, it has to be at least BETTER than the rest. - type: Item size: 15 sprite: Objects/Weapons/Melee/captain_sabre.rsi @@ -75,9 +76,10 @@ sprite: Objects/Weapons/Melee/claymore.rsi state: icon - type: MeleeWeapon + attackRate: 0.75 damage: types: - Slash: 25 + Slash: 33 - type: Item size: 20 - type: Clothing diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index bfeb7236f0..a46115c4d9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -15,13 +15,11 @@ - type: MeleeWeapon damage: types: - Blunt: 5 + Blunt: 7 bluntStaminaDamageFactor: 2.0 range: 1.5 - arcwidth: 60 - arc: default - cooldownTime: 1.5 - arcCooldownTime: 1.5 + angle: 60 + animation: WeaponArcSlash - type: StaminaDamageOnHit damage: 55 - type: Battery @@ -64,8 +62,7 @@ types: Blunt: 0 # why is this classed as a melee weapon? Is it needed for some interaction? range: 1 - arcWidth: 10 - arc: default + angle: 10 - type: Item size: 5 sprite: Objects/Weapons/Melee/flash.rsi diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml index 4fbef22d7a..fb43f7ecb0 100644 --- a/Resources/Prototypes/Entities/Structures/Windows/window.yml +++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml @@ -104,6 +104,11 @@ - type: Tag tags: - Window + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Effects/glass_hit.ogg" - type: Sprite drawdepth: Mobs netsync: false diff --git a/Resources/Prototypes/melee_weapon_animations.yml b/Resources/Prototypes/melee_weapon_animations.yml deleted file mode 100644 index da357b070b..0000000000 --- a/Resources/Prototypes/melee_weapon_animations.yml +++ /dev/null @@ -1,62 +0,0 @@ -- type: MeleeWeaponAnimation - id: default - state: slash - arcType: Slash - length: 0.1 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: bite - state: bite - arcType: Poke - length: 0.4 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: claw - state: claw - arcType: Slash - length: 0.4 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: disarm - state: disarm - arcType: Poke - length: 0.3 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: fist - state: fist - arcType: Poke - length: 0.15 - speed: 1 - -- type: MeleeWeaponAnimation - id: kick - state: kick - arcType: Poke - length: 0.3 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: punch - state: punch - arcType: Poke - length: 0.5 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 - -- type: MeleeWeaponAnimation - id: smash - state: smash - arcType: Poke - length: 0.3 - color: 255,255,255,1020 - colorDelta: 0,0,0,-5100 diff --git a/Resources/clientCommandPerms.yml b/Resources/clientCommandPerms.yml index d442ae9576..69c66b2875 100644 --- a/Resources/clientCommandPerms.yml +++ b/Resources/clientCommandPerms.yml @@ -42,7 +42,8 @@ - toggledecals - nodevis - nodevisfilter - - showspread + - showmeleespread + - showgunspread - showambient - showemergencyshuttle - zoom diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 1ad3947021..d33c0b5205 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -17,6 +17,11 @@ binds: type: State key: MouseLeft canFocus: true +- function: AltUse + type: State + key: MouseRight + canFocus: true + priority: -1 # UIRightClick & EditorCancelPlace should fire first. - function: ShowDebugMonitors type: Toggle key: F3 @@ -189,11 +194,6 @@ binds: - function: ReleasePulledObject type: State key: H -- function: OpenContextMenu - type: State - key: MouseRight - canFocus: true - priority: -1 # UIRightClick & EditorCancelPlace should fire first. - function: OpenCraftingMenu type: State key: G