diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 13032876b0..0321584f53 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -62,7 +62,6 @@ namespace Content.Client "EmitSoundOnUse", "FootstepModifier", "HeatResistance", - "CombatMode", "Teleportable", "ItemTeleporter", "Portal", diff --git a/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs index 244e81cf26..6bd54f02bb 100644 --- a/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/CameraRecoilComponent.cs @@ -29,6 +29,10 @@ namespace Content.Client.GameObjects.Components.Mobs private EyeComponent _eye; + // Basically I needed a way to chain this effect for the attack lunge animation. + // Sorry! + public Vector2 BaseOffset { get; set; } + public override void Initialize() { base.Initialize(); @@ -95,7 +99,7 @@ namespace Content.Client.GameObjects.Components.Mobs private void _updateEye() { - _eye.Offset = _currentKick; + _eye.Offset = BaseOffset + _currentKick; } } } diff --git a/Content.Client/GameObjects/Components/Mobs/CombatModeComponent.cs b/Content.Client/GameObjects/Components/Mobs/CombatModeComponent.cs new file mode 100644 index 0000000000..48ae173ad2 --- /dev/null +++ b/Content.Client/GameObjects/Components/Mobs/CombatModeComponent.cs @@ -0,0 +1,59 @@ +using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; + +namespace Content.Client.GameObjects.Components.Mobs +{ + [RegisterComponent] + public sealed class CombatModeComponent : SharedCombatModeComponent + { + [ViewVariables(VVAccess.ReadWrite)] + public bool IsInCombatMode { get; private set; } + + [ViewVariables(VVAccess.ReadWrite)] + public TargetingZone ActiveZone { get; private set; } + +#pragma warning disable 649 + [Dependency] private readonly IGameHud _gameHud; +#pragma warning restore 649 + + public override void HandleComponentState(ComponentState curState, ComponentState nextState) + { + base.HandleComponentState(curState, nextState); + + var state = (CombatModeComponentState) curState; + + IsInCombatMode = state.IsInCombatMode; + ActiveZone = state.TargetingZone; + UpdateHud(); + } + + public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null, IComponent component = null) + { + base.HandleMessage(message, netChannel, component); + + switch (message) + { + case PlayerAttachedMsg _: + _gameHud.CombatPanelVisible = true; + UpdateHud(); + break; + + case PlayerDetachedMsg _: + _gameHud.CombatPanelVisible = false; + break; + } + } + + private void UpdateHud() + { + _gameHud.CombatModeActive = IsInCombatMode; + _gameHud.TargetingZone = ActiveZone; + } + } +} diff --git a/Content.Client/GameObjects/Components/Mobs/MeleeLungeComponent.cs b/Content.Client/GameObjects/Components/Mobs/MeleeLungeComponent.cs new file mode 100644 index 0000000000..4dc93ac708 --- /dev/null +++ b/Content.Client/GameObjects/Components/Mobs/MeleeLungeComponent.cs @@ -0,0 +1,65 @@ +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components.Mobs +{ + [RegisterComponent] + public sealed class MeleeLungeComponent : Component + { + public override string Name => "MeleeLunge"; + + 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((BaseOffset, 0)); + offset *= (ResetTime - _time) / ResetTime; + } + + if (Owner.TryGetComponent(out CameraRecoilComponent recoilComponent)) + { + recoilComponent.BaseOffset = offset; + } + else if (Owner.TryGetComponent(out EyeComponent eyeComponent)) + { + eyeComponent.Offset = offset; + } + + if (Owner.TryGetComponent(out ISpriteComponent spriteComponent)) + { + // We have to account for rotation so the offset still checks out. + // SpriteComponent.Offset is applied before transform rotation (as expected). + var worldRotation = Owner.Transform.WorldRotation; + spriteComponent.Offset = new Angle(-worldRotation).RotateVec(offset); + } + + if (deleteSelf) + { + Owner.RemoveComponent(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Weapons/Melee/MeleeWeaponArcAnimationComponent.cs b/Content.Client/GameObjects/Components/Weapons/Melee/MeleeWeaponArcAnimationComponent.cs new file mode 100644 index 0000000000..f2e2964c2b --- /dev/null +++ b/Content.Client/GameObjects/Components/Weapons/Melee/MeleeWeaponArcAnimationComponent.cs @@ -0,0 +1,67 @@ +using Content.Shared.GameObjects.Components.Weapons.Melee; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components.Weapons.Melee +{ + [RegisterComponent] + public sealed class MeleeWeaponArcAnimationComponent : Component + { + public override string Name => "MeleeWeaponArcAnimation"; + + private MeleeWeaponAnimationPrototype _meleeWeaponAnimation; + + private float _timer; + private SpriteComponent _sprite; + private Angle _baseAngle; + + public override void Initialize() + { + base.Initialize(); + + _sprite = Owner.GetComponent(); + } + + public void SetData(MeleeWeaponAnimationPrototype prototype, Angle baseAngle) + { + _meleeWeaponAnimation = prototype; + _sprite.AddLayer(new RSI.StateId(prototype.State)); + _baseAngle = baseAngle; + } + + 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); + _sprite.Color = new Color(r, g, b, a); + + switch (_meleeWeaponAnimation.ArcType) + { + case WeaponArcType.Slash: + var angle = Angle.FromDegrees(_meleeWeaponAnimation.Width)/2; + Owner.Transform.LocalRotation = + _baseAngle + Angle.Lerp(-angle, angle, (float) (_timer / _meleeWeaponAnimation.Length.TotalSeconds)); + break; + + case WeaponArcType.Poke: + _sprite.Offset += (_meleeWeaponAnimation.Speed * frameTime, 0); + break; + } + + + if (_meleeWeaponAnimation.Length.TotalSeconds <= _timer) + { + Owner.Delete(); + } + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/CombatModeSystem.cs b/Content.Client/GameObjects/EntitySystems/CombatModeSystem.cs new file mode 100644 index 0000000000..850e61f477 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/CombatModeSystem.cs @@ -0,0 +1,33 @@ +using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.EntitySystemMessages; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.IoC; + +namespace Content.Client.GameObjects.EntitySystems +{ + public sealed class CombatModeSystem : EntitySystem + { +#pragma warning disable 649 + [Dependency] private readonly IGameHud _gameHud; +#pragma warning restore 649 + + public override void Initialize() + { + base.Initialize(); + + _gameHud.OnCombatModeChanged = OnCombatModeChanged; + _gameHud.OnTargetingZoneChanged = OnTargetingZoneChanged; + } + + private void OnTargetingZoneChanged(TargetingZone obj) + { + RaiseNetworkEvent(new CombatModeSystemMessages.SetTargetZoneMessage(obj)); + } + + private void OnCombatModeChanged(bool obj) + { + RaiseNetworkEvent(new CombatModeSystemMessages.SetCombatModeActiveMessage(obj)); + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/MeleeLungeSystem.cs b/Content.Client/GameObjects/EntitySystems/MeleeLungeSystem.cs new file mode 100644 index 0000000000..bff419b8ab --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/MeleeLungeSystem.cs @@ -0,0 +1,28 @@ +using Content.Client.GameObjects.Components.Mobs; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Client.GameObjects.EntitySystems +{ + [UsedImplicitly] + public sealed class MeleeLungeSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + EntityQuery = new TypeEntityQuery(); + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + foreach (var entity in RelevantEntities) + { + entity.GetComponent().Update(frameTime); + } + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs new file mode 100644 index 0000000000..93871ede55 --- /dev/null +++ b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -0,0 +1,101 @@ +using System.Linq; +using Content.Client.GameObjects.Components.Mobs; +using Content.Client.GameObjects.Components.Weapons.Melee; +using Content.Shared.GameObjects.Components.Weapons.Melee; +using JetBrains.Annotations; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Timers; +using static Content.Shared.GameObjects.EntitySystemMessages.MeleeWeaponSystemMessages; + +namespace Content.Client.GameObjects.EntitySystems +{ + [UsedImplicitly] + public sealed class MeleeWeaponSystem : EntitySystem + { +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; +#pragma warning restore 649 + + public override void Initialize() + { + base.Initialize(); + + EntityQuery = new TypeEntityQuery(typeof(MeleeWeaponArcAnimationComponent)); + } + + public override void RegisterMessageTypes() + { + base.RegisterMessageTypes(); + + RegisterMessageType(); + } + + public override void HandleNetMessage(INetChannel channel, EntitySystemMessage message) + { + base.HandleNetMessage(channel, message); + + switch (message) + { + case PlayMeleeWeaponAnimationMessage playMsg: + PlayWeaponArc(playMsg); + break; + } + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + foreach (var entity in RelevantEntities) + { + entity.GetComponent().Update(frameTime); + } + } + + private void PlayWeaponArc(PlayMeleeWeaponAnimationMessage msg) + { + if (!_prototypeManager.TryIndex(msg.ArcPrototype, out MeleeWeaponAnimationPrototype weaponArc)) + { + Logger.Error("Tried to play unknown weapon arc prototype '{0}'", msg.ArcPrototype); + return; + } + + var attacker = EntityManager.GetEntity(msg.Attacker); + + var lunge = attacker.EnsureComponent(); + lunge.SetData(msg.Angle); + + var entity = EntityManager.SpawnEntityAt("WeaponArc", attacker.Transform.GridPosition); + entity.Transform.LocalRotation = msg.Angle; + + var weaponArcAnimation = entity.GetComponent(); + weaponArcAnimation.SetData(weaponArc, msg.Angle); + + + foreach (var hitEntity in msg.Hits.Select(u => EntityManager.GetEntity(u))) + { + if (!hitEntity.TryGetComponent(out ISpriteComponent sprite)) continue; + + var originalColor = sprite.Color; + var newColor = Color.Red * originalColor; + sprite.Color = newColor; + + Timer.Spawn(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; + } + }); + } + } + } +} diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 13c5a43518..bf8a3749db 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -27,8 +27,7 @@ namespace Content.Client.Input human.AddFunction(ContentKeyFunctions.OpenCraftingMenu); human.AddFunction(ContentKeyFunctions.OpenInventoryMenu); human.AddFunction(ContentKeyFunctions.MouseMiddle); - // Disabled until there is feedback, so hitting tab doesn't suddenly break interaction. - // human.AddFunction(ContentKeyFunctions.ToggleCombatMode); + human.AddFunction(ContentKeyFunctions.ToggleCombatMode); var ghost = contexts.New("ghost", "common"); ghost.AddFunction(EngineKeyFunctions.MoveUp); diff --git a/Content.Client/UserInterface/GameHud.cs b/Content.Client/UserInterface/GameHud.cs index c8e7c70f53..3c4182b77b 100644 --- a/Content.Client/UserInterface/GameHud.cs +++ b/Content.Client/UserInterface/GameHud.cs @@ -1,5 +1,6 @@ using System; using Content.Client.Utility; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.Input; using Robust.Client.Graphics; using Robust.Client.Graphics.Drawing; @@ -48,6 +49,13 @@ namespace Content.Client.UserInterface Control HandsContainer { get; } Control InventoryQuickButtonContainer { get; } + bool CombatPanelVisible { get; set; } + bool CombatModeActive { get; set; } + TargetingZone TargetingZone { get; set; } + Action OnCombatModeChanged { get; set; } + Action OnTargetingZoneChanged { get; set; } + + // Init logic. void Initialize(); } @@ -62,6 +70,9 @@ namespace Content.Client.UserInterface private TopButton _buttonCraftingMenu; private TopButton _buttonSandboxMenu; private TutorialWindow _tutorialWindow; + private TargetingDoll _targetingDoll; + private Button _combatModeButton; + private VBoxContainer _combatPanelContainer; #pragma warning disable 649 [Dependency] private readonly IResourceCache _resourceCache; @@ -71,6 +82,26 @@ namespace Content.Client.UserInterface public Control HandsContainer { get; private set; } public Control InventoryQuickButtonContainer { get; private set; } + public bool CombatPanelVisible + { + get => _combatPanelContainer.Visible; + set => _combatPanelContainer.Visible = value; + } + + public bool CombatModeActive + { + get => _combatModeButton.Pressed; + set => _combatModeButton.Pressed = value; + } + + public TargetingZone TargetingZone + { + get => _targetingDoll.ActiveZone; + set => _targetingDoll.ActiveZone = value; + } + + public Action OnCombatModeChanged { get; set; } + public Action OnTargetingZoneChanged { get; set; } public void Initialize() { @@ -179,16 +210,35 @@ namespace Content.Client.UserInterface { GrowHorizontal = Control.GrowDirection.Begin, GrowVertical = Control.GrowDirection.Begin, + SizeFlagsVertical = Control.SizeFlags.ShrinkEnd }; HandsContainer = new MarginContainer { GrowHorizontal = Control.GrowDirection.Both, - GrowVertical = Control.GrowDirection.Begin + GrowVertical = Control.GrowDirection.Begin, + SizeFlagsVertical = Control.SizeFlags.ShrinkEnd }; + _combatPanelContainer = new VBoxContainer + { + Children = + { + (_combatModeButton = new Button + { + Text = _loc.GetString("Combat Mode"), + ToggleMode = true + }), + (_targetingDoll = new TargetingDoll(_resourceCache)) + } + }; + + _combatModeButton.OnToggled += args => OnCombatModeChanged?.Invoke(args.Pressed); + _targetingDoll.OnZoneChanged += args => OnTargetingZoneChanged?.Invoke(args); + inventoryContainer.Children.Add(HandsContainer); inventoryContainer.Children.Add(InventoryQuickButtonContainer); + inventoryContainer.Children.Add(_combatPanelContainer); } private void ButtonTutorialOnOnToggled() diff --git a/Content.Client/UserInterface/NanoStyle.cs b/Content.Client/UserInterface/NanoStyle.cs index b4ddfed847..fd0df7a970 100644 --- a/Content.Client/UserInterface/NanoStyle.cs +++ b/Content.Client/UserInterface/NanoStyle.cs @@ -481,6 +481,23 @@ namespace Content.Client.UserInterface { new StyleProperty(Label.StylePropertyFont, notoSansDisplayBold14), }), + + // Targeting doll + + new StyleRule(new SelectorElement(typeof(TextureButton), new []{TargetingDoll.StyleClassTargetDollZone}, null, new [] {TextureButton.StylePseudoClassNormal}), new [] + { + new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#F00")), + }), + + new StyleRule(new SelectorElement(typeof(TextureButton), new []{TargetingDoll.StyleClassTargetDollZone}, null, new [] {TextureButton.StylePseudoClassHover}), new [] + { + new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#0F0")), + }), + + new StyleRule(new SelectorElement(typeof(TextureButton), new []{TargetingDoll.StyleClassTargetDollZone}, null, new [] {TextureButton.StylePseudoClassPressed}), new [] + { + new StyleProperty(Control.StylePropertyModulateSelf, Color.FromHex("#00F")), + }), }); } } diff --git a/Content.Client/UserInterface/TargetingDoll.cs b/Content.Client/UserInterface/TargetingDoll.cs new file mode 100644 index 0000000000..e631be946c --- /dev/null +++ b/Content.Client/UserInterface/TargetingDoll.cs @@ -0,0 +1,82 @@ +using System; +using Content.Client.Utility; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Client.Interfaces.ResourceManagement; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.UserInterface +{ + public sealed class TargetingDoll : VBoxContainer + { + private TargetingZone _activeZone = TargetingZone.Middle; + public const string StyleClassTargetDollZone = "target-doll-zone"; + + private const string TextureHigh = "/Textures/UserInterface/target-doll-high.svg.96dpi.png"; + private const string TextureMiddle = "/Textures/UserInterface/target-doll-middle.svg.96dpi.png"; + private const string TextureLow = "/Textures/UserInterface/target-doll-low.svg.96dpi.png"; + + private readonly TextureButton _buttonHigh; + private readonly TextureButton _buttonMiddle; + private readonly TextureButton _buttonLow; + + public TargetingZone ActiveZone + { + get => _activeZone; + set + { + if (_activeZone == value) + { + return; + } + + _activeZone = value; + OnZoneChanged?.Invoke(value); + + UpdateButtons(); + } + } + + public event Action OnZoneChanged; + + public TargetingDoll(IResourceCache resourceCache) + { + _buttonHigh = new TextureButton + { + TextureNormal = resourceCache.GetTexture(TextureHigh), + StyleClasses = {StyleClassTargetDollZone}, + SizeFlagsHorizontal = SizeFlags.ShrinkCenter + }; + + _buttonMiddle = new TextureButton + { + TextureNormal = resourceCache.GetTexture(TextureMiddle), + StyleClasses = {StyleClassTargetDollZone}, + SizeFlagsHorizontal = SizeFlags.ShrinkCenter + }; + + _buttonLow = new TextureButton + { + TextureNormal = resourceCache.GetTexture(TextureLow), + StyleClasses = {StyleClassTargetDollZone}, + SizeFlagsHorizontal = SizeFlags.ShrinkCenter + }; + + _buttonHigh.OnPressed += _ => ActiveZone = TargetingZone.High; + _buttonMiddle.OnPressed += _ => ActiveZone = TargetingZone.Middle; + _buttonLow.OnPressed += _ => ActiveZone = TargetingZone.Low; + + AddChild(_buttonHigh); + AddChild(_buttonMiddle); + AddChild(_buttonLow); + + UpdateButtons(); + } + + private void UpdateButtons() + { + _buttonHigh.Pressed = _activeZone == TargetingZone.High; + _buttonMiddle.Pressed = _activeZone == TargetingZone.Middle; + _buttonLow.Pressed = _activeZone == TargetingZone.Low; + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/CombatModeComponent.cs b/Content.Server/GameObjects/Components/Mobs/CombatModeComponent.cs index d5078ec107..f8547b0830 100644 --- a/Content.Server/GameObjects/Components/Mobs/CombatModeComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/CombatModeComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.GameObjects.Components.Mobs; using Robust.Shared.GameObjects; using Robust.Shared.ViewVariables; @@ -9,11 +10,36 @@ namespace Content.Server.GameObjects.Components.Mobs /// using *everything* as a weapon. /// [RegisterComponent] - public sealed class CombatModeComponent : Component + public sealed class CombatModeComponent : SharedCombatModeComponent { - public override string Name => "CombatMode"; + private bool _isInCombatMode; + private TargetingZone _activeZone; [ViewVariables(VVAccess.ReadWrite)] - public bool IsInCombatMode { get; set; } + public bool IsInCombatMode + { + get => _isInCombatMode; + set + { + _isInCombatMode = value; + Dirty(); + } + } + + [ViewVariables(VVAccess.ReadWrite)] + public TargetingZone ActiveZone + { + get => _activeZone; + set + { + _activeZone = value; + Dirty(); + } + } + + public override ComponentState GetComponentState() + { + return new CombatModeComponentState(IsInCombatMode, ActiveZone); + } } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs index 6f9cb85961..622fc3cc64 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -1,43 +1,91 @@ -using Content.Server.GameObjects.EntitySystems; +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.GameObjects; +using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; +using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Melee { [RegisterComponent] public class MeleeWeaponComponent : Component, IAttack { + public override string Name => "MeleeWeapon"; + #pragma warning disable 649 [Dependency] private readonly IMapManager _mapManager; [Dependency] private readonly IServerEntityManager _serverEntityManager; + [Dependency] private readonly IEntitySystemManager _entitySystemManager; + [Dependency] private readonly IGameTiming _gameTiming; + [Dependency] private readonly IPrototypeManager _prototypeManager; #pragma warning restore 649 - public override string Name => "MeleeWeapon"; + private int _damage = 1; + private float _range = 1; + private float _arcWidth = 90; + private string _arc; + private string _hitSound; - public int Damage = 1; - public float Range = 1; - public float ArcWidth = 90; + [ViewVariables(VVAccess.ReadWrite)] + public string Arc + { + get => _arc; + set => _arc = value; + } + + [ViewVariables(VVAccess.ReadWrite)] + public float ArcWidth + { + get => _arcWidth; + set => _arcWidth = value; + } + + [ViewVariables(VVAccess.ReadWrite)] + public float Range + { + get => _range; + set => _range = value; + } + + [ViewVariables(VVAccess.ReadWrite)] + public int Damage + { + get => _damage; + set => _damage = value; + } public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); - serializer.DataField(ref Damage, "damage", 5); - serializer.DataField(ref Range, "range", 1); - serializer.DataField(ref ArcWidth, "arcwidth", 90); + serializer.DataField(ref _damage, "damage", 5); + serializer.DataField(ref _range, "range", 1); + serializer.DataField(ref _arcWidth, "arcwidth", 90); + serializer.DataField(ref _arc, "arc", "default"); + serializer.DataField(ref _hitSound, "hitSound", "/Audio/weapons/genhit1.ogg"); } void IAttack.Attack(AttackEventArgs eventArgs) { var location = eventArgs.User.Transform.GridPosition; - var angle = new Angle(eventArgs.ClickLocation.ToWorld(_mapManager).Position - location.ToWorld(_mapManager).Position); - var entities = _serverEntityManager.GetEntitiesInArc(eventArgs.User.Transform.GridPosition, Range, angle, ArcWidth); + var angle = new Angle(eventArgs.ClickLocation.ToWorld(_mapManager).Position - + location.ToWorld(_mapManager).Position); + // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. + var entities = + _serverEntityManager.GetEntitiesInArc(eventArgs.User.Transform.GridPosition, Range, angle, ArcWidth); + + var hitEntities = new List(); foreach (var entity in entities) { if (!entity.Transform.IsMapTransform || entity == eventArgs.User) @@ -46,8 +94,18 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee if (entity.TryGetComponent(out DamageableComponent damageComponent)) { damageComponent.TakeDamage(DamageType.Brute, Damage); + hitEntities.Add(entity); } } + + var audioSystem = _entitySystemManager.GetEntitySystem(); + audioSystem.Play(hitEntities.Count > 0 ? _hitSound : "/Audio/weapons/punchmiss.ogg"); + + if (Arc != null) + { + var sys = _entitySystemManager.GetEntitySystem(); + sys.SendAnimation(Arc, angle, eventArgs.User, hitEntities); + } } } } diff --git a/Content.Server/GameObjects/EntitySystems/CombatModeSystem.cs b/Content.Server/GameObjects/EntitySystems/CombatModeSystem.cs index 2e94e1eead..cf1c98670c 100644 --- a/Content.Server/GameObjects/EntitySystems/CombatModeSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/CombatModeSystem.cs @@ -1,15 +1,25 @@ using Content.Server.GameObjects.Components.Mobs; using Content.Shared.Input; +using JetBrains.Annotations; using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.IoC; using Robust.Shared.Players; +using static Content.Shared.GameObjects.EntitySystemMessages.CombatModeSystemMessages; namespace Content.Server.GameObjects.EntitySystems { + [UsedImplicitly] public sealed class CombatModeSystem : EntitySystem { +#pragma warning disable 649 + [Dependency] private readonly IPlayerManager _playerManager; +#pragma warning restore 649 + public override void Initialize() { base.Initialize(); @@ -19,7 +29,15 @@ namespace Content.Server.GameObjects.EntitySystems InputCmdHandler.FromDelegate(CombatModeToggled)); } - private void CombatModeToggled(ICommonSession session) + public override void RegisterMessageTypes() + { + base.RegisterMessageTypes(); + + RegisterMessageType(); + RegisterMessageType(); + } + + private static void CombatModeToggled(ICommonSession session) { var playerSession = (IPlayerSession) session; @@ -31,5 +49,28 @@ namespace Content.Server.GameObjects.EntitySystems combatModeComponent.IsInCombatMode = !combatModeComponent.IsInCombatMode; } + + public override void HandleNetMessage(INetChannel channel, EntitySystemMessage message) + { + base.HandleNetMessage(channel, message); + + var player = _playerManager.GetSessionByChannel(channel); + if (player.AttachedEntity == null + || !player.AttachedEntity.TryGetComponent(out CombatModeComponent combatModeComponent)) + { + return; + } + + switch (message) + { + case SetTargetZoneMessage setTargetZone: + combatModeComponent.ActiveZone = setTargetZone.TargetZone; + break; + + case SetCombatModeActiveMessage setActive: + combatModeComponent.IsInCombatMode = setActive.Active; + break; + } + } } } diff --git a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs new file mode 100644 index 0000000000..82ba72537a --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Shared.GameObjects.EntitySystemMessages; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Server.GameObjects.EntitySystems +{ + public sealed class MeleeWeaponSystem : EntitySystem + { + public void SendAnimation(string arc, Angle angle, IEntity attacker, IEnumerable hits) + { + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, + hits.Select(e => e.Uid).ToList())); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedCombatModeComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedCombatModeComponent.cs new file mode 100644 index 0000000000..6275aa8a38 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Mobs/SharedCombatModeComponent.cs @@ -0,0 +1,27 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Mobs +{ + public abstract class SharedCombatModeComponent : Component + { + public sealed override uint? NetID => ContentNetIDs.COMBATMODE; + public override string Name => "CombatMode"; + public sealed override Type StateType => typeof(CombatModeComponentState); + + [Serializable, NetSerializable] + protected sealed class CombatModeComponentState : ComponentState + { + public bool IsInCombatMode { get; } + public TargetingZone TargetingZone { get; } + + public CombatModeComponentState(bool isInCombatMode, TargetingZone targetingZone) + : base(ContentNetIDs.COMBATMODE) + { + IsInCombatMode = isInCombatMode; + TargetingZone = targetingZone; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Mobs/TargetingZone.cs b/Content.Shared/GameObjects/Components/Mobs/TargetingZone.cs new file mode 100644 index 0000000000..307c36b97c --- /dev/null +++ b/Content.Shared/GameObjects/Components/Mobs/TargetingZone.cs @@ -0,0 +1,27 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Mobs +{ + /// + /// Zones the player can target for attacks. + /// + [Serializable, NetSerializable] + public enum TargetingZone + { + /// + /// Torso/arm area. + /// + Middle, + + /// + /// Legs/groin area. + /// + Low, + + /// + /// Go for the head. + /// + High + } +} diff --git a/Content.Shared/GameObjects/Components/Weapons/Melee/MeleeWeaponAnimationPrototype.cs b/Content.Shared/GameObjects/Components/Weapons/Melee/MeleeWeaponAnimationPrototype.cs new file mode 100644 index 0000000000..765cb0882b --- /dev/null +++ b/Content.Shared/GameObjects/Components/Weapons/Melee/MeleeWeaponAnimationPrototype.cs @@ -0,0 +1,59 @@ +using System; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.GameObjects.Components.Weapons.Melee +{ + [Prototype("MeleeWeaponAnimation")] + public sealed class MeleeWeaponAnimationPrototype : IPrototype, IIndexedPrototype + { + private string _state; + private string _id; + private Vector4 _colorDelta; + private Vector4 _color; + private TimeSpan _length; + private float _speed; + private float _width; + private WeaponArcType _arcType; + + [ViewVariables] public string ID => _id; + [ViewVariables] public string State => _state; + [ViewVariables] public TimeSpan Length => _length; + [ViewVariables] public float Speed => _speed; + [ViewVariables] public Vector4 Color => _color; + [ViewVariables] public Vector4 ColorDelta => _colorDelta; + [ViewVariables] public WeaponArcType ArcType => _arcType; + [ViewVariables] public float Width => _width; + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + + serializer.DataField(ref _state, "state", null); + serializer.DataField(ref _id, "id", null); + serializer.DataField(ref _colorDelta, "colorDelta", Vector4.Zero); + serializer.DataField(ref _color, "color", new Vector4(1, 1, 1, 1)); + if (serializer.TryReadDataField("length", out float length)) + { + _length = TimeSpan.FromSeconds(length); + } + else + { + _length = TimeSpan.FromSeconds(0.5f); + } + + serializer.DataField(ref _speed, "speed", 1); + serializer.DataField(ref _arcType, "arcType", WeaponArcType.Slash); + serializer.DataField(ref _width, "width", 90); + } + } + + public enum WeaponArcType + { + Slash, + Poke, + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 28d43f282a..3387bb90e4 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -28,5 +28,6 @@ public const uint TECHNOLOGY_DATABASE = 1022; public const uint RESEARCH_CONSOLE = 1023; public const uint WIRES = 1024; + public const uint COMBATMODE = 1025; } } diff --git a/Content.Shared/GameObjects/EntitySystemMessages/CombatModeSystemMessages.cs b/Content.Shared/GameObjects/EntitySystemMessages/CombatModeSystemMessages.cs new file mode 100644 index 0000000000..814d844c22 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystemMessages/CombatModeSystemMessages.cs @@ -0,0 +1,32 @@ +using System; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.EntitySystemMessages +{ + public static class CombatModeSystemMessages + { + [Serializable, NetSerializable] + public sealed class SetTargetZoneMessage : EntitySystemMessage + { + public SetTargetZoneMessage(TargetingZone targetZone) + { + TargetZone = targetZone; + } + + public TargetingZone TargetZone { get; } + } + + [Serializable, NetSerializable] + public sealed class SetCombatModeActiveMessage : EntitySystemMessage + { + public SetCombatModeActiveMessage(bool active) + { + Active = active; + } + + public bool Active { get; } + } + } +} diff --git a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs new file mode 100644 index 0000000000..9f250b4b24 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.EntitySystemMessages +{ + public static class MeleeWeaponSystemMessages + { + [Serializable, NetSerializable] + public sealed class PlayMeleeWeaponAnimationMessage : EntitySystemMessage + { + public PlayMeleeWeaponAnimationMessage(string arcPrototype, Angle angle, EntityUid attacker, List hits) + { + ArcPrototype = arcPrototype; + Angle = angle; + Attacker = attacker; + Hits = hits; + } + + public string ArcPrototype { get; } + public Angle Angle { get; } + public EntityUid Attacker { get; } + public List Hits { get; } + } + } +} diff --git a/Resources/Audio/weapons/bladeslice.ogg b/Resources/Audio/weapons/bladeslice.ogg new file mode 100644 index 0000000000..ab8154ab9b Binary files /dev/null and b/Resources/Audio/weapons/bladeslice.ogg differ diff --git a/Resources/Audio/weapons/genhit1.ogg b/Resources/Audio/weapons/genhit1.ogg new file mode 100644 index 0000000000..65cd446c04 Binary files /dev/null and b/Resources/Audio/weapons/genhit1.ogg differ diff --git a/Resources/Audio/weapons/genhit2.ogg b/Resources/Audio/weapons/genhit2.ogg new file mode 100644 index 0000000000..1e06911e20 Binary files /dev/null and b/Resources/Audio/weapons/genhit2.ogg differ diff --git a/Resources/Audio/weapons/genhit3.ogg b/Resources/Audio/weapons/genhit3.ogg new file mode 100644 index 0000000000..dd514bd179 Binary files /dev/null and b/Resources/Audio/weapons/genhit3.ogg differ diff --git a/Resources/Audio/weapons/punch1.ogg b/Resources/Audio/weapons/punch1.ogg new file mode 100644 index 0000000000..bf0f95fe20 Binary files /dev/null and b/Resources/Audio/weapons/punch1.ogg differ diff --git a/Resources/Audio/weapons/punch2.ogg b/Resources/Audio/weapons/punch2.ogg new file mode 100644 index 0000000000..11a7dbffd7 Binary files /dev/null and b/Resources/Audio/weapons/punch2.ogg differ diff --git a/Resources/Audio/weapons/punch3.ogg b/Resources/Audio/weapons/punch3.ogg new file mode 100644 index 0000000000..010b7a8ad3 Binary files /dev/null and b/Resources/Audio/weapons/punch3.ogg differ diff --git a/Resources/Audio/weapons/punch4.ogg b/Resources/Audio/weapons/punch4.ogg new file mode 100644 index 0000000000..667ae68532 Binary files /dev/null and b/Resources/Audio/weapons/punch4.ogg differ diff --git a/Resources/Audio/weapons/punchmiss.ogg b/Resources/Audio/weapons/punchmiss.ogg new file mode 100644 index 0000000000..4f1e0e99b2 Binary files /dev/null and b/Resources/Audio/weapons/punchmiss.ogg differ diff --git a/Resources/Audio/weapons/smash.ogg b/Resources/Audio/weapons/smash.ogg new file mode 100644 index 0000000000..0bcbfbfe2c Binary files /dev/null and b/Resources/Audio/weapons/smash.ogg differ diff --git a/Resources/Prototypes/Entities/Effects/WeaponArc.yml b/Resources/Prototypes/Entities/Effects/WeaponArc.yml new file mode 100644 index 0000000000..e465b34542 --- /dev/null +++ b/Resources/Prototypes/Entities/Effects/WeaponArc.yml @@ -0,0 +1,10 @@ +- type: entity + id: WeaponArc + save: false + components: + - type: Sprite + sprite: Effects/weapons/arcs.rsi + directional: false + offset: 0.85, 0 + drawdepth: Overlays + - type: MeleeWeaponArcAnimation diff --git a/Resources/Prototypes/Entities/items/toolbox.yml b/Resources/Prototypes/Entities/items/toolbox.yml index 63d20fcd60..8cd4b41ce0 100644 --- a/Resources/Prototypes/Entities/items/toolbox.yml +++ b/Resources/Prototypes/Entities/items/toolbox.yml @@ -1,6 +1,17 @@ +- type: entity + id: ToolboxBase + parent: BaseItem + components: + - type: Storage + Capacity: 60 + - type: Item + Size: 9999 + - type: MeleeWeapon + hitSound: "/Audio/weapons/smash.ogg" + - type: entity name: Emergency Toolbox - parent: BaseItem + parent: ToolboxBase id: RedToolboxItem description: A bright red toolbox, stocked with emergency tools components: @@ -8,14 +19,10 @@ texture: Objects/Tools/toolbox_r.png - type: Icon texture: Objects/Tools/toolbox_r.png - - type: Storage - Capacity: 60 - - type: Item - Size: 9999 - type: entity name: Mechanical Toolbox - parent: BaseItem + parent: ToolboxBase id: BlueToolboxItem description: A blue box, stocked with mechanical tools components: @@ -23,14 +30,10 @@ texture: Objects/Tools/Toolbox_b.png - type: Icon texture: Objects/Tools/Toolbox_b.png - - type: Storage - Capacity: 60 - - type: Item - Size: 9999 - type: entity name: Electrical Toolbox - parent: BaseItem + parent: ToolboxBase id: YellowToolboxItem description: A toolbox typically stocked with electrical gear components: @@ -38,10 +41,6 @@ texture: Objects/Tools/Toolbox_y.png - type: Icon texture: Objects/Tools/Toolbox_y.png - - type: Storage - Capacity: 60 - - type: Item - Size: 9999 - type: entity id: YellowToolboxItemFilled diff --git a/Resources/Prototypes/Entities/items/weapons/spear.yml b/Resources/Prototypes/Entities/items/weapons/spear.yml index c820c7273d..5595c69dea 100644 --- a/Resources/Prototypes/Entities/items/weapons/spear.yml +++ b/Resources/Prototypes/Entities/items/weapons/spear.yml @@ -12,7 +12,18 @@ state: spear - type: MeleeWeapon + range: 1.5 + arcwidth: 10 + arc: spear + - type: Item Size: 24 sprite: Objects/Melee/spear.rsi prefix: inhand + +- type: MeleeWeaponAnimation + id: spear + state: spear + length: 0.10 + speed: 6 + arcType: Poke diff --git a/Resources/Prototypes/MeleeWeaponAnimations/default.yml b/Resources/Prototypes/MeleeWeaponAnimations/default.yml new file mode 100644 index 0000000000..c7fe845719 --- /dev/null +++ b/Resources/Prototypes/MeleeWeaponAnimations/default.yml @@ -0,0 +1,7 @@ +- type: MeleeWeaponAnimation + id: default + state: slash + arcType: Slash + length: 0.1 + color: 255,255,255,1020 + colorDelta: 0,0,0,-5100 diff --git a/Resources/Textures/Effects/weapons/arcs.rsi/meta.json b/Resources/Textures/Effects/weapons/arcs.rsi/meta.json new file mode 100644 index 0000000000..d9ae4e1a8d --- /dev/null +++ b/Resources/Textures/Effects/weapons/arcs.rsi/meta.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + + "states": [ + { + "name": "spear", + "directions": 1 + }, + { + "name": "slash", + "directions": 1 + } + ] +} diff --git a/Resources/Textures/Effects/weapons/arcs.rsi/slash.png b/Resources/Textures/Effects/weapons/arcs.rsi/slash.png new file mode 100644 index 0000000000..8336ae54bd Binary files /dev/null and b/Resources/Textures/Effects/weapons/arcs.rsi/slash.png differ diff --git a/Resources/Textures/Effects/weapons/arcs.rsi/spear.png b/Resources/Textures/Effects/weapons/arcs.rsi/spear.png new file mode 100644 index 0000000000..1ab400ef46 Binary files /dev/null and b/Resources/Textures/Effects/weapons/arcs.rsi/spear.png differ diff --git a/Resources/Textures/UserInterface/target-doll-high.svg b/Resources/Textures/UserInterface/target-doll-high.svg new file mode 100644 index 0000000000..2218df9d4c --- /dev/null +++ b/Resources/Textures/UserInterface/target-doll-high.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/UserInterface/target-doll-high.svg.96dpi.png b/Resources/Textures/UserInterface/target-doll-high.svg.96dpi.png new file mode 100644 index 0000000000..73cdfe4e85 Binary files /dev/null and b/Resources/Textures/UserInterface/target-doll-high.svg.96dpi.png differ diff --git a/Resources/Textures/UserInterface/target-doll-low.svg b/Resources/Textures/UserInterface/target-doll-low.svg new file mode 100644 index 0000000000..c1f40a97a1 --- /dev/null +++ b/Resources/Textures/UserInterface/target-doll-low.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/Resources/Textures/UserInterface/target-doll-low.svg.96dpi.png b/Resources/Textures/UserInterface/target-doll-low.svg.96dpi.png new file mode 100644 index 0000000000..cc3965b57b Binary files /dev/null and b/Resources/Textures/UserInterface/target-doll-low.svg.96dpi.png differ diff --git a/Resources/Textures/UserInterface/target-doll-middle.svg b/Resources/Textures/UserInterface/target-doll-middle.svg new file mode 100644 index 0000000000..ba9cc83246 --- /dev/null +++ b/Resources/Textures/UserInterface/target-doll-middle.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/Resources/Textures/UserInterface/target-doll-middle.svg.96dpi.png b/Resources/Textures/UserInterface/target-doll-middle.svg.96dpi.png new file mode 100644 index 0000000000..684c74ecb3 Binary files /dev/null and b/Resources/Textures/UserInterface/target-doll-middle.svg.96dpi.png differ diff --git a/Resources/Textures/UserInterface/target-doll.svg b/Resources/Textures/UserInterface/target-doll.svg new file mode 100644 index 0000000000..b86f3a7878 --- /dev/null +++ b/Resources/Textures/UserInterface/target-doll.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 0ddaea0ee7..3e6cf46cd8 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -82,7 +82,7 @@ binds: key: MouseRight canFocus: true - function: ToggleCombatMode - type: Toggle + type: State key: Tab - function: OpenCraftingMenu type: state