Melee refactor (#10897)

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-09-29 15:51:59 +10:00
committed by GitHub
parent c583b7b361
commit f51248ecaa
140 changed files with 2440 additions and 1824 deletions

View File

@@ -155,7 +155,7 @@ namespace Content.Client.Administration.Systems
if (function == EngineKeyFunctions.UIClick) if (function == EngineKeyFunctions.UIClick)
_clientConsoleHost.ExecuteCommand($"vv {uid}"); _clientConsoleHost.ExecuteCommand($"vv {uid}");
else if (function == ContentKeyFunctions.OpenContextMenu) else if (function == EngineKeyFunctions.AltUse)
_verbSystem.VerbMenu.OpenVerbMenu(uid, true); _verbSystem.VerbMenu.OpenVerbMenu(uid, true);
else else
return; return;
@@ -173,7 +173,7 @@ namespace Content.Client.Administration.Systems
if (function == EngineKeyFunctions.UIClick) if (function == EngineKeyFunctions.UIClick)
_clientConsoleHost.ExecuteCommand($"vv {uid}"); _clientConsoleHost.ExecuteCommand($"vv {uid}");
else if (function == ContentKeyFunctions.OpenContextMenu) else if (function == EngineKeyFunctions.AltUse)
_verbSystem.VerbMenu.OpenVerbMenu(uid, true); _verbSystem.VerbMenu.OpenVerbMenu(uid, true);
else else
return; return;

View File

@@ -54,7 +54,7 @@ namespace Content.Client.Administration.UI.CustomControls
if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label) if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label)
label.Text = GetText(selectedPlayer); label.Text = GetText(selectedPlayer);
} }
else if (args.Event.Function == ContentKeyFunctions.OpenContextMenu) else if (args.Event.Function == EngineKeyFunctions.AltUse)
{ {
_verbSystem.VerbMenu.OpenVerbMenu(selectedPlayer.EntityUid); _verbSystem.VerbMenu.OpenVerbMenu(selectedPlayer.EntityUid);
} }

View File

@@ -4,8 +4,8 @@ using Content.Shared.Targeting;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
namespace Content.Client.CombatMode namespace Content.Client.CombatMode
{ {
@@ -23,6 +23,16 @@ namespace Content.Client.CombatMode
SubscribeLocalEvent<CombatModeComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached()); SubscribeLocalEvent<CombatModeComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached());
SubscribeLocalEvent<CombatModeComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached()); SubscribeLocalEvent<CombatModeComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached());
SubscribeLocalEvent<SharedCombatModeComponent, ComponentHandleState>(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() public override void Shutdown()
@@ -33,8 +43,12 @@ namespace Content.Client.CombatMode
public bool IsInCombatMode() public bool IsInCombatMode()
{ {
return EntityManager.TryGetComponent(_playerManager.LocalPlayer?.ControlledEntity, out CombatModeComponent? combatMode) && var entity = _playerManager.LocalPlayer?.ControlledEntity;
combatMode.IsInCombatMode;
if (entity == null)
return false;
return IsInCombatMode(entity.Value);
} }
private void OnTargetingZoneChanged(TargetingZone obj) private void OnTargetingZoneChanged(TargetingZone obj)
@@ -42,8 +56,4 @@ namespace Content.Client.CombatMode
EntityManager.RaisePredictiveEvent(new CombatModeSystemMessages.SetTargetZoneMessage(obj)); EntityManager.RaisePredictiveEvent(new CombatModeSystemMessages.SetTargetZoneMessage(obj));
} }
} }
public static class A
{
}
} }

View File

@@ -1,10 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.CombatMode;
using Content.Client.Examine; using Content.Client.Examine;
using Content.Client.Gameplay; using Content.Client.Gameplay;
using Content.Client.Verbs; using Content.Client.Verbs;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Content.Shared.Input; using Content.Shared.Input;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
@@ -45,6 +47,7 @@ namespace Content.Client.ContextMenu.UI
private readonly VerbSystem _verbSystem; private readonly VerbSystem _verbSystem;
private readonly ExamineSystem _examineSystem; private readonly ExamineSystem _examineSystem;
private readonly SharedCombatModeSystem _combatMode;
/// <summary> /// <summary>
/// This maps the currently displayed entities to the actual GUI elements. /// This maps the currently displayed entities to the actual GUI elements.
@@ -59,12 +62,13 @@ namespace Content.Client.ContextMenu.UI
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_verbSystem = verbSystem; _verbSystem = verbSystem;
_examineSystem = EntitySystem.Get<ExamineSystem>(); _examineSystem = _entityManager.EntitySysManager.GetEntitySystem<ExamineSystem>();
_combatMode = _entityManager.EntitySysManager.GetEntitySystem<CombatModeSystem>();
_cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true); _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
CommandBinds.Builder CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenContextMenu, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true)) .Bind(EngineKeyFunctions.AltUse, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true))
.Register<EntityMenuPresenter>(); .Register<EntityMenuPresenter>();
} }
@@ -109,7 +113,7 @@ namespace Content.Client.ContextMenu.UI
return; return;
// open verb menu? // open verb menu?
if (args.Function == ContentKeyFunctions.OpenContextMenu) if (args.Function == EngineKeyFunctions.AltUse)
{ {
_verbSystem.VerbMenu.OpenVerbMenu(entity.Value); _verbSystem.VerbMenu.OpenVerbMenu(entity.Value);
args.Handle(); args.Handle();
@@ -160,6 +164,9 @@ namespace Content.Client.ContextMenu.UI
if (_stateManager.CurrentState is not GameplayStateBase) if (_stateManager.CurrentState is not GameplayStateBase)
return false; return false;
if (_combatMode.IsInCombatMode(args.Session?.AttachedEntity))
return false;
var coords = args.Coordinates.ToMap(_entityManager); var coords = args.Coordinates.ToMap(_entityManager);
if (_verbSystem.TryGetEntityMenuEntities(coords, out var entities)) if (_verbSystem.TryGetEntityMenuEntities(coords, out var entities))

View File

@@ -50,10 +50,6 @@ public sealed class DoAfterOverlay : Overlay
} }
var worldPosition = _transform.GetWorldPosition(xform); var worldPosition = _transform.GetWorldPosition(xform);
if (!args.WorldAABB.Contains(worldPosition))
continue;
var index = 0; var index = 0;
var worldMatrix = Matrix3.CreateTranslation(worldPosition); var worldMatrix = Matrix3.CreateTranslation(worldPosition);

View File

@@ -1,8 +1,7 @@
namespace Content.Client.Effects; namespace Content.Client.Effects;
/// <summary>
/// Deletes the attached entity whenever any animation completes. Used for temporary client-side entities.
/// </summary>
[RegisterComponent] [RegisterComponent]
public sealed class EffectVisualsComponent : Component public sealed class EffectVisualsComponent : Component {}
{
public float Length;
public float Accumulator = 0f;
}

View File

@@ -27,7 +27,6 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.TakeScreenshot); common.AddFunction(ContentKeyFunctions.TakeScreenshot);
common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI); common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI);
common.AddFunction(ContentKeyFunctions.Point); common.AddFunction(ContentKeyFunctions.Point);
common.AddFunction(ContentKeyFunctions.OpenContextMenu);
// Not in engine, because engine cannot check for sanbox/admin status before starting placement. // Not in engine, because engine cannot check for sanbox/admin status before starting placement.
common.AddFunction(ContentKeyFunctions.EditorCopyObject); common.AddFunction(ContentKeyFunctions.EditorCopyObject);

View File

@@ -11,6 +11,7 @@ using Content.Shared.Interaction;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
@@ -70,7 +71,7 @@ namespace Content.Client.Items.Managers
_entitySystemManager.GetEntitySystem<ExamineSystem>() _entitySystemManager.GetEntitySystem<ExamineSystem>()
.DoExamine(item.Value); .DoExamine(item.Value);
} }
else if (args.Function == ContentKeyFunctions.OpenContextMenu) else if (args.Function == EngineKeyFunctions.AltUse)
{ {
_entitySystemManager.GetEntitySystem<VerbSystem>().VerbMenu.OpenVerbMenu(item.Value); _entitySystemManager.GetEntitySystem<VerbSystem>().VerbMenu.OpenVerbMenu(item.Value);
} }

View File

@@ -12,7 +12,6 @@ namespace Content.Client.Items.Systems;
public sealed class ItemSystem : SharedItemSystem public sealed class ItemSystem : SharedItemSystem
{ {
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly IResourceCache _resCache = default!; [Dependency] private readonly IResourceCache _resCache = default!;
public override void Initialize() public override void Initialize()
@@ -30,8 +29,8 @@ public sealed class ItemSystem : SharedItemSystem
public override void VisualsChanged(EntityUid uid) 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 the item is in a container, it might be equipped to hands or inventory slots --> update visuals.
if (_containerSystem.TryGetContainingContainer(uid, out var container)) if (Container.TryGetContainingContainer(uid, out var container))
RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID), true); RaiseLocalEvent(container.Owner, new VisualsChangedEvent(uid, container.ID));
} }
/// <summary> /// <summary>

View File

@@ -105,6 +105,7 @@ namespace Content.Client.Options.UI.Tabs
AddHeader("ui-options-header-interaction-basic"); AddHeader("ui-options-header-interaction-basic");
AddButton(EngineKeyFunctions.Use); AddButton(EngineKeyFunctions.Use);
AddButton(EngineKeyFunctions.AltUse);
AddButton(ContentKeyFunctions.UseItemInHand); AddButton(ContentKeyFunctions.UseItemInHand);
AddButton(ContentKeyFunctions.AltUseItemInHand); AddButton(ContentKeyFunctions.AltUseItemInHand);
AddButton(ContentKeyFunctions.ActivateItemInWorld); AddButton(ContentKeyFunctions.ActivateItemInWorld);
@@ -134,7 +135,6 @@ namespace Content.Client.Options.UI.Tabs
AddButton(ContentKeyFunctions.CycleChatChannelForward); AddButton(ContentKeyFunctions.CycleChatChannelForward);
AddButton(ContentKeyFunctions.CycleChatChannelBackward); AddButton(ContentKeyFunctions.CycleChatChannelBackward);
AddButton(ContentKeyFunctions.OpenCharacterMenu); AddButton(ContentKeyFunctions.OpenCharacterMenu);
AddButton(ContentKeyFunctions.OpenContextMenu);
AddButton(ContentKeyFunctions.OpenCraftingMenu); AddButton(ContentKeyFunctions.OpenCraftingMenu);
AddButton(ContentKeyFunctions.OpenInventoryMenu); AddButton(ContentKeyFunctions.OpenInventoryMenu);
AddButton(ContentKeyFunctions.OpenInfo); AddButton(ContentKeyFunctions.OpenInfo);

View File

@@ -37,6 +37,11 @@ namespace Content.Client.Rotation
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
var sprite = entMan.GetComponent<ISpriteComponent>(component.Owner); var sprite = entMan.GetComponent<ISpriteComponent>(component.Owner);
if (sprite.Rotation.Equals(rotation))
{
return;
}
if (!entMan.TryGetComponent(sprite.Owner, out AnimationPlayerComponent? animation)) if (!entMan.TryGetComponent(sprite.Owner, out AnimationPlayerComponent? animation))
{ {
sprite.Rotation = rotation; sprite.Rotation = rotation;

View File

@@ -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<IEntityManager>();
if (entMan.TryGetComponent(Owner, out ISpriteComponent? spriteComponent))
{
spriteComponent.Offset = offset;
}
if (deleteSelf)
{
entMan.RemoveComponent<MeleeLungeComponent>(Owner);
}
}
}
}

View File

@@ -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<SpriteComponent>(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<TransformComponent>(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<TransformComponent>(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);
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Content.Client.Weapons.Melee.Components;
/// <summary>
/// Used for melee attack animations. Typically just has a fadeout.
/// </summary>
[RegisterComponent]
public sealed class WeaponArcVisualsComponent : Component
{
[ViewVariables, DataField("animation")]
public WeaponArcAnimation Animation = WeaponArcAnimation.None;
}
public enum WeaponArcAnimation : byte
{
None,
Thrust,
Slash,
}

View File

@@ -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;
/// <summary>
/// Debug overlay showing the arc and range of a melee weapon.
/// </summary>
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<TransformComponent>(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);
}
}

View File

@@ -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<MeleeLungeComponent>(true))
{
meleeLungeComponent.Update(frameTime);
}
}
}
}

View File

@@ -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<IOverlayManager>();
if (overlayManager.RemoveOverlay<MeleeArcOverlay>())
{
return;
}
var sysManager = collection.Resolve<IEntitySystemManager>();
overlayManager.AddOverlay(new MeleeArcOverlay(
collection.Resolve<IEntityManager>(),
collection.Resolve<IEyeManager>(),
collection.Resolve<IInputManager>(),
collection.Resolve<IPlayerManager>(),
sysManager.GetEntitySystem<MeleeWeaponSystem>(),
sysManager.GetEntitySystem<SharedCombatModeSystem>()));
}
}

View File

@@ -2,31 +2,17 @@ using Content.Shared.Weapons;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Weapons.Melee; namespace Content.Client.Weapons.Melee;
public sealed partial class MeleeWeaponSystem public sealed partial class MeleeWeaponSystem
{ {
private static readonly Animation DefaultDamageAnimation = new() /// <summary>
{ /// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register.
Length = TimeSpan.FromSeconds(DamageAnimationLength), /// </summary>
AnimationTracks = private const float DamageAnimationLength = 0.30f;
{
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;
private const string DamageAnimationKey = "damage-effect"; private const string DamageAnimationKey = "damage-effect";
private void InitializeEffect() private void InitializeEffect()
@@ -47,27 +33,25 @@ public sealed partial class MeleeWeaponSystem
/// <summary> /// <summary>
/// Gets the red effect animation whenever the server confirms something is hit /// Gets the red effect animation whenever the server confirms something is hit
/// </summary> /// </summary>
public Animation? GetDamageAnimation(EntityUid uid, SpriteComponent? sprite = null) private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null)
{ {
if (!Resolve(uid, ref sprite, false)) if (!Resolve(uid, ref sprite, false))
return null; return null;
// 90% of them are going to be this so why allocate a new class. // 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 return new Animation
{ {
Length = TimeSpan.FromSeconds(DamageAnimationLength), Length = TimeSpan.FromSeconds(DamageAnimationLength),
AnimationTracks = AnimationTracks =
{ {
new AnimationTrackComponentProperty() new AnimationTrackComponentProperty
{ {
ComponentType = typeof(SpriteComponent), ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Color), Property = nameof(SpriteComponent.Color),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames = KeyFrames =
{ {
new AnimationTrackProperty.KeyFrame(Color.Red * sprite.Color, 0f), new AnimationTrackProperty.KeyFrame(color * sprite.Color, 0f),
new AnimationTrackProperty.KeyFrame(sprite.Color, DamageAnimationLength) new AnimationTrackProperty.KeyFrame(sprite.Color, DamageAnimationLength)
} }
} }
@@ -77,36 +61,44 @@ public sealed partial class MeleeWeaponSystem
private void OnDamageEffect(DamageEffectEvent ev) private void OnDamageEffect(DamageEffectEvent ev)
{ {
if (Deleted(ev.Entity)) var color = ev.Color;
return;
var player = EnsureComp<AnimationPlayerComponent>(ev.Entity); foreach (var ent in ev.Entities)
// 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))
{ {
_animation.Stop(ev.Entity, player, DamageAnimationKey); if (Deleted(ent))
{
continue;
}
var player = EnsureComp<AnimationPlayerComponent>(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<SpriteComponent>(ent, out var sprite))
{
continue;
}
if (TryComp<DamageEffectComponent>(ent, out var effect))
{
sprite.Color = effect.Color;
}
var animation = GetDamageAnimation(ent, color, sprite);
if (animation == null)
continue;
var comp = EnsureComp<DamageEffectComponent>(ent);
comp.NetSyncEnabled = false;
comp.Color = sprite.Color;
_animation.Play(player, animation, DamageAnimationKey);
} }
if (!TryComp<SpriteComponent>(ev.Entity, out var sprite))
{
return;
}
if (TryComp<DamageEffectComponent>(ev.Entity, out var effect))
{
sprite.Color = effect.Color;
}
var animation = GetDamageAnimation(ev.Entity, sprite);
if (animation == null)
return;
var comp = EnsureComp<DamageEffectComponent>(ev.Entity);
comp.NetSyncEnabled = false;
comp.Color = sprite.Color;
_animation.Play(player, DefaultDamageAnimation, DamageAnimationKey);
} }
} }

View File

@@ -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.Client.Weapons.Melee.Components;
using Content.Shared.Examine;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using JetBrains.Annotations; using Content.Shared.Weapons.Melee.Events;
using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameObjects; using Robust.Client.Graphics;
using Robust.Shared.IoC; using Robust.Client.Input;
using Robust.Shared.Log; using Robust.Client.Player;
using Robust.Shared.Maths; 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.Prototypes;
using Robust.Shared.Timing; 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!; base.Initialize();
[Dependency] private readonly IGameTiming _gameTiming = default!; InitializeEffect();
[Dependency] private readonly AnimationPlayerSystem _animation = default!; _overlayManager.AddOverlay(new MeleeWindupOverlay(EntityManager, _timing, _protoManager, _cache));
[Dependency] private readonly EffectSystem _effectSystem = default!; SubscribeNetworkEvent<DamageEffectEvent>(OnDamageEffect);
SubscribeNetworkEvent<MeleeLungeEvent>(OnMeleeLunge);
}
public override void Initialize() public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<MeleeWindupOverlay>();
}
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(); weapon.Attacking = false;
SubscribeNetworkEvent<PlayMeleeWeaponAnimationMessage>(PlayWeaponArc); if (weapon.WindUpStart != null)
SubscribeNetworkEvent<PlayLungeAnimationMessage>(PlayLunge);
SubscribeNetworkEvent<DamageEffectEvent>(OnDamageEffect);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
foreach (var arcAnimationComponent in EntityManager.EntityQuery<MeleeWeaponArcAnimationComponent>(true))
{ {
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; return;
} }
var attacker = msg.Attacker; // If it's an unarmed attack then do a disarm
if (!EntityManager.EntityExists(msg.Attacker)) if (weapon.Owner == entity)
{ {
// FIXME: This should never happen. EntityUid? target = null;
Logger.Error($"Tried to play a weapon arc {msg.ArcPrototype}, but the attacker does not exist. attacker={msg.Attacker}, source={msg.Source}");
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; return;
} }
if (!Deleted(attacker)) // Otherwise do heavy attack if it's a weapon.
// Start a windup
if (weapon.WindUpStart == null)
{ {
var lunge = attacker.EnsureComponent<MeleeLungeComponent>(); EntityManager.RaisePredictiveEvent(new StartHeavyAttackEvent(weapon.Owner));
lunge.SetData(msg.Angle); weapon.WindUpStart = currentTime;
var entity = EntityManager.SpawnEntity(weaponArc.Prototype, EntityManager.GetComponent<TransformComponent>(attacker).Coordinates);
EntityManager.GetComponent<TransformComponent>(entity).LocalRotation = msg.Angle;
var weaponArcAnimation = EntityManager.GetComponent<MeleeWeaponArcAnimationComponent>(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<TransformComponent>(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);
}
} }
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)) EntityManager.RaisePredictiveEvent(new HeavyAttackEvent(weapon.Owner, coordinates));
{
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;
}
});
} }
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<MeleeLungeComponent>().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 else
{ {
// FIXME: This should never happen. coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager);
Logger.Error($"Tried to play a lunge animation, but the entity \"{msg.Source}\" does not exist."); }
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<CombatModeComponent>(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<HandsComponent>(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);
}
/// <summary>
/// Does all of the melee effects for a player that are predicted, i.e. character lunge and weapon animation.
/// </summary>
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<SpriteComponent>(animationUid, out var sprite))
{
sprite[0].AutoAnimated = false;
if (TryComp<WeaponArcVisualsComponent>(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),
}
}
}
};
}
/// <summary>
/// Get the fadeout for static weapon arcs.
/// </summary>
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)
}
}
}
};
}
/// <summary>
/// Get the sprite offset animation to use for mob lunges.
/// </summary>
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)
}
}
}
};
}
} }

View File

@@ -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<SharedTransformSystem>();
_texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png");
_shader = protoManager.Index<ShaderPrototype>("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<SpriteComponent>();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
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<MeleeWeaponComponent>(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);
}
}

View File

@@ -5,7 +5,7 @@ namespace Content.Client.Weapons.Ranged;
public sealed class ShowSpreadCommand : IConsoleCommand public sealed class ShowSpreadCommand : IConsoleCommand
{ {
public string Command => "showspread"; public string Command => "showgunspread";
public string Description => $"Shows gun spread overlay for debugging"; public string Description => $"Shows gun spread overlay for debugging";
public string Help => $"{Command}"; public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args) public void Execute(IConsoleShell shell, string argStr, string[] args)

View File

@@ -13,11 +13,11 @@ public sealed class GunSpreadOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpace; public override OverlaySpace Space => OverlaySpace.WorldSpace;
private IEntityManager _entManager; private IEntityManager _entManager;
private IEyeManager _eye; private readonly IEyeManager _eye;
private IGameTiming _timing; private readonly IGameTiming _timing;
private IInputManager _input; private readonly IInputManager _input;
private IPlayerManager _player; private readonly IPlayerManager _player;
private GunSystem _guns; private readonly GunSystem _guns;
public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGameTiming timing, IInputManager input, IPlayerManager player, GunSystem system) public GunSpreadOverlay(IEntityManager entManager, IEyeManager eyeManager, IGameTiming timing, IInputManager input, IPlayerManager player, GunSystem system)
{ {

View File

@@ -6,11 +6,9 @@ using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Weapons.Melee;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Reflection; using Robust.Shared.Reflection;
@@ -78,18 +76,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem));
var attack = false;
var interactUsing = false; var interactUsing = false;
var interactHand = false; var interactHand = false;
await server.WaitAssertion(() => 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.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; };
testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; };
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, false, target);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target);
Assert.That(attack);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand); Assert.That(interactHand);
@@ -144,18 +138,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem));
var attack = false;
var interactUsing = false; var interactUsing = false;
var interactHand = false; var interactHand = false;
await server.WaitAssertion(() => 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.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; };
testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; };
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, false, target);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target);
Assert.That(attack, Is.False);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand, Is.False); Assert.That(interactHand, Is.False);
@@ -208,18 +198,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem));
var attack = false;
var interactUsing = false; var interactUsing = false;
var interactHand = false; var interactHand = false;
await server.WaitAssertion(() => 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.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; };
testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; };
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, false, target);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target);
Assert.That(attack);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand); Assert.That(interactHand);
@@ -273,18 +259,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<InteractionSystem>(out var interactionSystem));
Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem)); Assert.That(entitySystemManager.TryGetEntitySystem<TestInteractionSystem>(out var testInteractionSystem));
var attack = false;
var interactUsing = false; var interactUsing = false;
var interactHand = false; var interactHand = false;
await server.WaitAssertion(() => 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.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactUsing = true; };
testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(target)); interactHand = true; };
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, false, target);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target);
Assert.That(attack, Is.False);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand, Is.False); Assert.That(interactHand, Is.False);
@@ -344,7 +326,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await server.WaitIdleAsync(); await server.WaitIdleAsync();
var attack = false;
var interactUsing = false; var interactUsing = false;
var interactHand = false; var interactHand = false;
await server.WaitAssertion(() => await server.WaitAssertion(() =>
@@ -352,19 +333,14 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
Assert.That(container.Insert(user)); Assert.That(container.Insert(user));
Assert.That(sEntities.GetComponent<TransformComponent>(user).Parent.Owner, Is.EqualTo(containerEntity)); Assert.That(sEntities.GetComponent<TransformComponent>(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.InteractUsingEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactUsing = true; };
testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactHand = true; }; testInteractionSystem.InteractHandEvent = (ev) => { Assert.That(ev.Target, Is.EqualTo(containerEntity)); interactHand = true; };
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, false, target);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(target).Coordinates, target);
Assert.That(attack, Is.False);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand, Is.False); Assert.That(interactHand, Is.False);
interactionSystem.DoAttack(user, sEntities.GetComponent<TransformComponent>(containerEntity).Coordinates, false, containerEntity);
interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(containerEntity).Coordinates, containerEntity); interactionSystem.UserInteraction(user, sEntities.GetComponent<TransformComponent>(containerEntity).Coordinates, containerEntity);
Assert.That(attack);
Assert.That(interactUsing, Is.False); Assert.That(interactUsing, Is.False);
Assert.That(interactHand); Assert.That(interactHand);
@@ -383,14 +359,12 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
[Reflect(false)] [Reflect(false)]
public sealed class TestInteractionSystem : EntitySystem public sealed class TestInteractionSystem : EntitySystem
{ {
public ComponentEventHandler<HandsComponent, ClickAttackEvent>? AttackEvent;
public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent; public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent;
public EntityEventHandler<InteractHandEvent>? InteractHandEvent; public EntityEventHandler<InteractHandEvent>? InteractHandEvent;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<HandsComponent, ClickAttackEvent>((u, c, e) => AttackEvent?.Invoke(u, c, e));
SubscribeLocalEvent<InteractUsingEvent>((e) => InteractUsingEvent?.Invoke(e)); SubscribeLocalEvent<InteractUsingEvent>((e) => InteractUsingEvent?.Invoke(e));
SubscribeLocalEvent<InteractHandEvent>((e) => InteractHandEvent?.Invoke(e)); SubscribeLocalEvent<InteractHandEvent>((e) => InteractHandEvent?.Invoke(e));
} }

View File

@@ -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 Content.Server.Damage.Events;
using Robust.Shared.Audio; using Content.Server.Weapons.Melee.Events;
using Robust.Shared.Player; using Content.Shared.Weapons.Melee;
using Robust.Shared.Random;
using Robust.Shared.Containers; using Robust.Shared.Containers;
namespace Content.Server.Abilities.Boxer namespace Content.Server.Abilities.Boxer
@@ -24,25 +17,22 @@ namespace Content.Server.Abilities.Boxer
SubscribeLocalEvent<BoxingGlovesComponent, StaminaMeleeHitEvent>(OnStamHit); SubscribeLocalEvent<BoxingGlovesComponent, StaminaMeleeHitEvent>(OnStamHit);
} }
private void OnInit(EntityUid uid, BoxerComponent boxer, ComponentInit args) private void OnInit(EntityUid uid, BoxerComponent component, ComponentInit args)
{ {
if (TryComp<MeleeWeaponComponent>(uid, out var meleeComp)) if (TryComp<MeleeWeaponComponent>(uid, out var meleeComp))
meleeComp.Range *= boxer.RangeBonus; meleeComp.Range *= component.RangeBonus;
} }
private void GetDamageModifiers(EntityUid uid, BoxerComponent component, ItemMeleeDamageEvent args) 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); args.ModifiersList.Add(component.UnarmedModifiers);
} }
private void OnStamHit(EntityUid uid, BoxingGlovesComponent component, StaminaMeleeHitEvent args) private void OnStamHit(EntityUid uid, BoxingGlovesComponent component, StaminaMeleeHitEvent args)
{ {
_containerSystem.TryGetContainingContainer(uid, out var equipee); if (!_containerSystem.TryGetContainingContainer(uid, out var equipee))
if (TryComp<BoxerComponent>(equipee?.Owner, out var boxer)) return;
if (TryComp<BoxerComponent>(equipee.Owner, out var boxer))
args.Multiplier *= boxer.BoxingGlovesModifier; args.Multiplier *= boxer.BoxingGlovesModifier;
} }
} }

View File

@@ -3,7 +3,7 @@ using Content.Server.Atmos.Components;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems; using Content.Server.Temperature.Systems;
using Content.Server.Weapon.Melee; using Content.Server.Weapons.Melee.Events;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;

View File

@@ -1,7 +1,6 @@
using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems; using Content.Server.Chemistry.EntitySystems;
using Content.Server.Interaction.Components; using Content.Server.Interaction.Components;
using Content.Server.Weapon.Melee;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
@@ -12,6 +11,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Server.Interaction; using Content.Server.Interaction;
using Content.Server.Weapons.Melee;
namespace Content.Server.Chemistry.Components namespace Content.Server.Chemistry.Components
{ {
@@ -78,7 +78,8 @@ namespace Content.Server.Chemistry.Components
target.Value.PopupMessage(Loc.GetString("hypospray-component-feel-prick-message")); target.Value.PopupMessage(Loc.GetString("hypospray-component-feel-prick-message"));
var meleeSys = EntitySystem.Get<MeleeWeaponSystem>(); var meleeSys = EntitySystem.Get<MeleeWeaponSystem>();
var angle = Angle.FromWorldVec(_entMan.GetComponent<TransformComponent>(target.Value).WorldPosition - _entMan.GetComponent<TransformComponent>(user).WorldPosition); var angle = Angle.FromWorldVec(_entMan.GetComponent<TransformComponent>(target.Value).WorldPosition - _entMan.GetComponent<TransformComponent>(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); SoundSystem.Play(_injectSound.GetSound(), Filter.Pvs(user), user);

View File

@@ -1,7 +1,10 @@
using System.Linq;
using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Components;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Chemistry.EntitySystems namespace Content.Server.Chemistry.EntitySystems
{ {
@@ -10,7 +13,7 @@ namespace Content.Server.Chemistry.EntitySystems
private void InitializeHypospray() private void InitializeHypospray()
{ {
SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<HyposprayComponent, ClickAttackEvent>(OnClickAttack); SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
SubscribeLocalEvent<HyposprayComponent, SolutionChangedEvent>(OnSolutionChange); SubscribeLocalEvent<HyposprayComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand); SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
} }
@@ -39,12 +42,12 @@ namespace Content.Server.Chemistry.EntitySystems
comp.TryDoInject(target, user); 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; return;
comp.TryDoInject(args.Target.Value, args.User); comp.TryDoInject(args.HitEntities.First(), args.User);
} }
} }
} }

View File

@@ -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.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Stunnable;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Physics;
namespace Content.Server.CombatMode namespace Content.Server.CombatMode
{ {
[UsedImplicitly] [UsedImplicitly]
public sealed class CombatModeSystem : SharedCombatModeSystem 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SharedCombatModeComponent, DisarmActionEvent>(OnEntityActionPerform); SubscribeLocalEvent<SharedCombatModeComponent, ComponentGetState>(OnGetState);
} }
private void OnEntityActionPerform(EntityUid uid, SharedCombatModeComponent component, DisarmActionEvent args) private void OnGetState(EntityUid uid, SharedCombatModeComponent component, ref ComponentGetState args)
{ {
if (args.Handled) args.State = new CombatModeComponentState(component.IsInCombatMode, component.ActiveZone);
return;
if (!_actionBlockerSystem.CanAttack(args.Performer))
return;
if (TryComp<HandsComponent>(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<HandsComponent>(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<DisarmProneComponent>(disarmer))
return 1.0f;
if (HasComp<DisarmProneComponent>(disarmed))
return 0.0f;
var contestResults = 1 - _contests.OverallStrengthContest(disarmer, disarmed);
float chance = (disarmerComp.BaseDisarmFailChance + contestResults);
if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
{
chance += malus.Malus;
}
return Math.Clamp(chance, 0f, 1f);
} }
} }
} }

View File

@@ -14,7 +14,7 @@ namespace Content.Server.Contests
/// >1 = Advantage to roller /// >1 = Advantage to roller
/// <1 = Advantage to target /// <1 = Advantage to target
/// Roller should be the entity with an advantage from being bigger/healthier/more skilled, etc. /// Roller should be the entity with an advantage from being bigger/healthier/more skilled, etc.
/// <summary> /// </summary>
public sealed class ContestsSystem : EntitySystem public sealed class ContestsSystem : EntitySystem
{ {
[Dependency] private readonly SharedMobStateSystem _mobStateSystem = default!; [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)) if (!Resolve(roller, ref rollerPhysics, false) || !Resolve(target, ref targetPhysics, false))
return 1f; return 1f;
if (rollerPhysics == null || targetPhysics == null)
return 1f;
if (targetPhysics.FixturesMass == 0) if (targetPhysics.FixturesMass == 0)
return 1f; return 1f;
return (rollerPhysics.FixturesMass / targetPhysics.FixturesMass); return rollerPhysics.FixturesMass / targetPhysics.FixturesMass;
} }
/// <summary> /// <summary>
@@ -47,18 +44,15 @@ namespace Content.Server.Contests
if (!Resolve(roller, ref rollerDamage, false) || !Resolve(target, ref targetDamage, false)) if (!Resolve(roller, ref rollerDamage, false) || !Resolve(target, ref targetDamage, false))
return 1f; return 1f;
if (rollerDamage == null || targetDamage == null)
return 1f;
// First, we'll see what health they go into crit at. // First, we'll see what health they go into crit at.
float rollerThreshold = 100f; float rollerThreshold = 100f;
float targetThreshold = 100f; float targetThreshold = 100f;
if (TryComp<MobStateComponent>(roller, out var rollerState) && rollerState != null && if (TryComp<MobStateComponent>(roller, out var rollerState) &&
_mobStateSystem.TryGetEarliestIncapacitatedState(rollerState, 10000, out _, out var rollerCritThreshold)) _mobStateSystem.TryGetEarliestIncapacitatedState(rollerState, 10000, out _, out var rollerCritThreshold))
rollerThreshold = (float) rollerCritThreshold; rollerThreshold = (float) rollerCritThreshold;
if (TryComp<MobStateComponent>(target, out var targetState) && targetState != null && if (TryComp<MobStateComponent>(target, out var targetState) &&
_mobStateSystem.TryGetEarliestIncapacitatedState(targetState, 10000, out _, out var targetCritThreshold)) _mobStateSystem.TryGetEarliestIncapacitatedState(targetState, 10000, out _, out var targetCritThreshold))
targetThreshold = (float) targetCritThreshold; targetThreshold = (float) targetCritThreshold;
@@ -97,8 +91,9 @@ namespace Content.Server.Contests
var massMultiplier = massWeight / weightTotal; var massMultiplier = massWeight / weightTotal;
var stamMultiplier = stamWeight / weightTotal; var stamMultiplier = stamWeight / weightTotal;
return ((DamageContest(roller, target) * damageMultiplier) + (MassContest(roller, target) * massMultiplier) return DamageContest(roller, target) * damageMultiplier +
+ (StaminaContest(roller, target) * stamMultiplier)); MassContest(roller, target) * massMultiplier +
StaminaContest(roller, target) * stamMultiplier;
} }
/// <summary> /// <summary>
@@ -109,6 +104,7 @@ namespace Content.Server.Contests
{ {
return score switch return score switch
{ {
// TODO: Should just be a curve
<= 0 => 1f, <= 0 => 1f,
<= 0.25f => 0.9f, <= 0.25f => 0.9f,
<= 0.5f => 0.75f, <= 0.5f => 0.75f,

View File

@@ -7,10 +7,4 @@ public sealed class StaminaDamageOnHitComponent : Component
{ {
[ViewVariables(VVAccess.ReadWrite), DataField("damage")] [ViewVariables(VVAccess.ReadWrite), DataField("damage")]
public float Damage = 30f; public float Damage = 30f;
/// <summary>
/// Play a sound when this knocks down an entity.
/// </summary>
[DataField("knockdownSound")]
public SoundSpecifier? KnockdownSound;
} }

View File

@@ -1,9 +1,9 @@
using Content.Server.Damage.Components; using Content.Server.Damage.Components;
using Content.Server.Damage.Events; using Content.Server.Damage.Events;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Weapon.Melee;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.CombatMode; using Content.Server.CombatMode;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Rounding; using Content.Shared.Rounding;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
@@ -123,7 +123,7 @@ public sealed class StaminaSystem : EntitySystem
foreach (var comp in toHit) foreach (var comp in toHit)
{ {
var oldDamage = comp.StaminaDamage; 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)) if (comp.StaminaDamage.Equals(oldDamage))
{ {
_popup.PopupEntity(Loc.GetString("stamina-resist"), comp.Owner, Filter.Entities(args.User)); _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); _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; if (!Resolve(uid, ref component, false) || component.Critical) return;
@@ -181,8 +181,6 @@ public sealed class StaminaSystem : EntitySystem
{ {
if (component.StaminaDamage >= component.CritThreshold) if (component.StaminaDamage >= component.CritThreshold)
{ {
if (knockdownSound != null)
SoundSystem.Play(knockdownSound.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, knockdownSound.Params);
EnterStamCrit(uid, component); EnterStamCrit(uid, component);
} }
} }

View File

@@ -21,6 +21,7 @@ using Content.Shared.StatusEffect;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Content.Shared.Tag; using Content.Shared.Tag;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;

View File

@@ -7,7 +7,6 @@ namespace Content.Server.Entry
"ConstructionGhost", "ConstructionGhost",
"IconSmooth", "IconSmooth",
"InteractionOutline", "InteractionOutline",
"MeleeWeaponArcAnimation",
"AnimationsTest", "AnimationsTest",
"ItemStatus", "ItemStatus",
"Marker", "Marker",

View File

@@ -1,7 +1,7 @@
using Content.Server.Flash.Components; using Content.Server.Flash.Components;
using Content.Server.Light.EntitySystems; using Content.Server.Light.EntitySystems;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Weapon.Melee; using Content.Server.Weapons.Melee.Events;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Flash; using Content.Shared.Flash;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;

View File

@@ -1,18 +1,14 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Hands.Components;
using Content.Server.Pulling; using Content.Server.Pulling;
using Content.Server.Storage.Components; using Content.Server.Storage.Components;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DragDrop; using Content.Shared.DragDrop;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Interaction; 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.Pulling.Components;
using Content.Shared.Weapons.Melee; using Content.Shared.Storage;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -20,7 +16,6 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Random; using Robust.Shared.Random;
using static Content.Shared.Storage.SharedStorageComponent;
namespace Content.Server.Interaction namespace Content.Server.Interaction
{ {
@@ -34,9 +29,8 @@ namespace Content.Server.Interaction
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly PullingSystem _pullSystem = default!; [Dependency] private readonly PullingSystem _pullSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -61,7 +55,7 @@ namespace Content.Server.Interaction
if (Deleted(target)) if (Deleted(target))
return false; return false;
if (!target.TryGetContainer(out var container)) if (!_container.TryGetContainingContainer(target, out var container))
return false; return false;
if (!TryComp(container.Owner, out ServerStorageComponent? storage)) if (!TryComp(container.Owner, out ServerStorageComponent? storage))
@@ -74,7 +68,7 @@ namespace Content.Server.Interaction
return false; return false;
// we don't check if the user can access the storage entity itself. This should be handed by the UI system. // 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 #region Drag drop
@@ -132,21 +126,6 @@ namespace Content.Server.Interaction
} }
#endregion #endregion
/// <summary>
/// Entity will try and use their active hand at the target location.
/// Don't use for players
/// </summary>
/// <param name="entity"></param>
/// <param name="coords"></param>
/// <param name="uid"></param>
internal void AiUseInteraction(EntityUid entity, EntityCoordinates coords, EntityUid uid)
{
if (HasComp<ActorComponent>(entity))
throw new InvalidOperationException();
UserInteraction(entity, coords, uid);
}
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{ {
if (!ValidateClientInput(session, coords, uid, out var userEntity)) if (!ValidateClientInput(session, coords, uid, out var userEntity))
@@ -169,103 +148,5 @@ namespace Content.Server.Interaction
return _pullSystem.TogglePull(userEntity.Value, pull); 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<ItemComponent>(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<MeleeWeaponComponent>(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);
}
}
} }
} }

View File

@@ -7,7 +7,7 @@ using Content.Server.Doors.Components;
using Content.Server.Magic.Events; using Content.Server.Magic.Events;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Spawners.Components; 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;
using Content.Shared.Actions.ActionTypes; using Content.Shared.Actions.ActionTypes;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;

View File

@@ -1,8 +1,8 @@
using Content.Server.CombatMode; using Content.Server.CombatMode;
using Content.Server.NPC.Components; using Content.Server.NPC.Components;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.Weapons.Melee;
namespace Content.Server.NPC.Systems; namespace Content.Server.NPC.Systems;
@@ -64,7 +64,7 @@ public sealed partial class NPCCombatSystem
return; return;
} }
if (weapon.CooldownEnd > _timing.CurTime) if (weapon.NextAttack > _timing.CurTime)
{ {
return; return;
} }
@@ -84,6 +84,6 @@ public sealed partial class NPCCombatSystem
return; return;
} }
_interaction.DoAttack(component.Owner, targetXform.Coordinates, false, component.Target); _melee.AttemptLightAttack(component.Owner, weapon, component.Target);
} }
} }

View File

@@ -1,7 +1,8 @@
using Content.Server.Interaction; using Content.Server.Interaction;
using Content.Server.Weapon.Ranged.Systems; using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -17,6 +18,7 @@ public sealed partial class NPCCombatSystem : EntitySystem
[Dependency] private readonly GunSystem _gun = default!; [Dependency] private readonly GunSystem _gun = default!;
[Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize() public override void Initialize()

View File

@@ -1,19 +1,17 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Projectiles.Components; using Content.Server.Projectiles.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Camera; using Content.Shared.Camera;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Vehicle.Components;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
using GunSystem = Content.Server.Weapon.Ranged.Systems.GunSystem;
namespace Content.Server.Projectiles namespace Content.Server.Projectiles
{ {
@@ -52,7 +50,7 @@ namespace Content.Server.Projectiles
{ {
if (modifiedDamage.Total > FixedPoint2.Zero) if (modifiedDamage.Total > FixedPoint2.Zero)
{ {
RaiseNetworkEvent(new DamageEffectEvent(otherEntity), Filter.Pvs(otherEntity, entityManager: EntityManager)); RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {otherEntity}), Filter.Pvs(otherEntity, entityManager: EntityManager));
} }
_adminLogger.Add(LogType.BulletHit, _adminLogger.Add(LogType.BulletHit,

View File

@@ -28,6 +28,7 @@ using Content.Server.Popups;
using Content.Shared.Destructible; using Content.Shared.Destructible;
using static Content.Shared.Storage.SharedStorageComponent; using static Content.Shared.Storage.SharedStorageComponent;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.CombatMode;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
namespace Content.Server.Storage.EntitySystems namespace Content.Server.Storage.EntitySystems
@@ -48,6 +49,7 @@ namespace Content.Server.Storage.EntitySystems
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
/// <inheritdoc /> /// <inheritdoc />
@@ -263,6 +265,9 @@ namespace Content.Server.Storage.EntitySystems
/// <returns></returns> /// <returns></returns>
private void OnActivate(EntityUid uid, ServerStorageComponent storageComp, ActivateInWorldEvent args) 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) if (TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked)
return; return;

View File

@@ -5,7 +5,7 @@ using Content.Server.Power.Components;
using Content.Server.Power.Events; using Content.Server.Power.Events;
using Content.Server.Speech.EntitySystems; using Content.Server.Speech.EntitySystems;
using Content.Server.Stunnable.Components; using Content.Server.Stunnable.Components;
using Content.Server.Weapon.Melee; using Content.Server.Weapons.Melee.Events;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;

View File

@@ -3,7 +3,7 @@ using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems; using Content.Server.Chemistry.EntitySystems;
using Content.Server.Tools.Components; using Content.Server.Tools.Components;
using Content.Server.Weapon.Melee; using Content.Server.Weapons.Melee.Events;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Cargo.Systems; using Content.Server.Cargo.Systems;
using Content.Server.EUI; using Content.Server.EUI;
@@ -5,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.Materials; using Content.Shared.Materials;
using Content.Shared.Research.Prototypes; using Content.Shared.Research.Prototypes;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Content.Shared.Weapons.Melee;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -16,7 +18,7 @@ public sealed class StatValuesCommand : IConsoleCommand
{ {
public string Command => "showvalues"; public string Command => "showvalues";
public string Description => "Dumps all stats for a particular category into a table."; public string Description => "Dumps all stats for a particular category into a table.";
public string Help => $"{Command} <cargosell / lathsell>"; public string Help => $"{Command} <cargosell / lathsell / melee>";
public void Execute(IConsoleShell shell, string argStr, string[] args) public void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
if (shell.Player is not IPlayerSession pSession) if (shell.Player is not IPlayerSession pSession)
@@ -41,6 +43,9 @@ public sealed class StatValuesCommand : IConsoleCommand
case "lathesell": case "lathesell":
message = GetLatheMessage(); message = GetLatheMessage();
break; break;
case "melee":
message = GetMelee();
break;
default: default:
shell.WriteError($"{args[0]} is not a valid stat!"); shell.WriteError($"{args[0]} is not a valid stat!");
return; return;
@@ -100,6 +105,51 @@ public sealed class StatValuesCommand : IConsoleCommand
return state; return state;
} }
private StatValuesEuiMessage GetMelee()
{
var compFactory = IoCManager.Resolve<IComponentFactory>();
var protoManager = IoCManager.Resolve<IPrototypeManager>();
var values = new List<string[]>();
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
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<string>()
{
"ID",
"Price",
},
Values = values,
};
return state;
}
private StatValuesEuiMessage GetLatheMessage() private StatValuesEuiMessage GetLatheMessage()
{ {
var values = new List<string[]>(); var values = new List<string[]>();

View File

@@ -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;
}
}

View File

@@ -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<MeleeWeaponComponent, HandSelectedEvent>(OnHandSelected);
SubscribeLocalEvent<MeleeWeaponComponent, ClickAttackEvent>(OnClickAttack);
SubscribeLocalEvent<MeleeWeaponComponent, WideAttackEvent>(OnWideAttack);
SubscribeLocalEvent<MeleeWeaponComponent, GetVerbsEvent<ExamineVerb>>(OnMeleeExaminableVerb);
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
}
private void OnMeleeExaminableVerb(EntityUid uid, MeleeWeaponComponent component, GetVerbsEvent<ExamineVerb> 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<EntityUid>() { 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));
}
/// <summary>
/// Set the melee weapon cooldown's end to the specified value. Will use the maximum of the existing cooldown or the new one.
/// </summary>
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<TransformComponent>(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<TransformComponent>(args.User).WorldPosition, angle, comp.ArcWidth, comp.Range, EntityManager.GetComponent<TransformComponent>(owner).MapID, args.User);
var hitEntities = new List<EntityUid>();
foreach (var entity in entities)
{
if (entity.IsInContainer() || entity == args.User)
continue;
if (EntityManager.HasComponent<DamageableComponent>(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<MeleeSoundComponent>(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<EntityUid> 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<EntityUid>();
for (var i = 0; i < increments; i++)
{
var castAngle = new Angle(baseAngle + increment * i);
var res = Get<SharedPhysicsSystem>().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<BloodstreamComponent>();
foreach (var entity in args.HitEntities)
{
if (Deleted(entity))
continue;
if (EntityManager.TryGetComponent<BloodstreamComponent?>(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<EntityUid> 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
{
/// <summary>
/// The base amount of damage dealt by the melee hit.
/// </summary>
public readonly DamageSpecifier BaseDamage = new();
/// <summary>
/// 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.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// Damage to add to the default melee weapon damage. Applied before modifiers.
/// </summary>
/// <remarks>
/// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier.
/// </remarks>
public DamageSpecifier BonusDamage = new();
public ItemMeleeDamageEvent(DamageSpecifier baseDamage)
{
BaseDamage = baseDamage;
}
}
/// <summary>
/// Raised directed on the melee weapon entity used to attack something in combat mode,
/// whether through a click attack or wide attack.
/// </summary>
public sealed class MeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// The base amount of damage dealt by the melee hit.
/// </summary>
public readonly DamageSpecifier BaseDamage = new();
/// <summary>
/// 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.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// Damage to add to the default melee weapon damage. Applied before modifiers.
/// </summary>
/// <remarks>
/// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier.
/// </remarks>
public DamageSpecifier BonusDamage = new();
/// <summary>
/// A list containing every hit entity. Can be zero.
/// </summary>
public IEnumerable<EntityUid> HitEntities { get; }
/// <summary>
/// Used to define a new hit sound in case you want to override the default GenericHit.
/// Also gets a pitch modifier added to it.
/// </summary>
public SoundSpecifier? HitSoundOverride {get; set;}
/// <summary>
/// The user who attacked with the melee weapon.
/// </summary>
public EntityUid User { get; }
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, DamageSpecifier baseDamage)
{
HitEntities = hitEntities;
User = user;
BaseDamage = baseDamage;
}
}
}

View File

@@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Weapon.Melee.Components; namespace Content.Server.Weapons.Melee.Components;
/// <summary> /// <summary>
/// Plays the specified sound upon receiving damage of the specified type. /// Plays the specified sound upon receiving damage of the specified type.

View File

@@ -1,7 +1,7 @@
using Content.Shared.Damage; using Content.Shared.Damage;
using Robust.Shared.Audio; using Robust.Shared.Audio;
namespace Content.Server.Weapon.Melee.EnergySword namespace Content.Server.Weapons.Melee.EnergySword.Components
{ {
[RegisterComponent] [RegisterComponent]
internal sealed class EnergySwordComponent : Component internal sealed class EnergySwordComponent : Component

View File

@@ -1,6 +1,7 @@
using Content.Server.CombatMode.Disarm; using Content.Server.CombatMode.Disarm;
using Content.Server.Kitchen.Components; 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;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Item; using Content.Shared.Item;
@@ -9,11 +10,12 @@ using Content.Shared.Light.Component;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Content.Shared.Toggleable; using Content.Shared.Toggleable;
using Content.Shared.Tools.Components; using Content.Shared.Tools.Components;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Weapon.Melee.EnergySword namespace Content.Server.Weapons.Melee.EnergySword
{ {
public sealed class EnergySwordSystem : EntitySystem public sealed class EnergySwordSystem : EntitySystem
{ {

View File

@@ -0,0 +1,30 @@
using Content.Shared.Damage;
namespace Content.Server.Weapons.Melee.Events;
public sealed class ItemMeleeDamageEvent : HandledEntityEventArgs
{
/// <summary>
/// The base amount of damage dealt by the melee hit.
/// </summary>
public readonly DamageSpecifier BaseDamage = new();
/// <summary>
/// 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.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// Damage to add to the default melee weapon damage. Applied before modifiers.
/// </summary>
/// <remarks>
/// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier.
/// </remarks>
public DamageSpecifier BonusDamage = new();
public ItemMeleeDamageEvent(DamageSpecifier baseDamage)
{
BaseDamage = baseDamage;
}
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
namespace Content.Server.Weapons.Melee.Events;
/// <summary>
/// Raised directed on the melee weapon entity used to attack something in combat mode,
/// whether through a click attack or wide attack.
/// </summary>
public sealed class MeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// The base amount of damage dealt by the melee hit.
/// </summary>
public readonly DamageSpecifier BaseDamage = new();
/// <summary>
/// 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.
/// </summary>
public List<DamageModifierSet> ModifiersList = new();
/// <summary>
/// Damage to add to the default melee weapon damage. Applied before modifiers.
/// </summary>
/// <remarks>
/// This might be required as damage modifier sets cannot add a new damage type to a DamageSpecifier.
/// </remarks>
public DamageSpecifier BonusDamage = new();
/// <summary>
/// A list containing every hit entity. Can be zero.
/// </summary>
public IEnumerable<EntityUid> HitEntities { get; }
/// <summary>
/// Used to define a new hit sound in case you want to override the default GenericHit.
/// Also gets a pitch modifier added to it.
/// </summary>
public SoundSpecifier? HitSoundOverride {get; set;}
/// <summary>
/// The user who attacked with the melee weapon.
/// </summary>
public EntityUid User { get; }
public MeleeHitEvent(List<EntityUid> hitEntities, EntityUid user, DamageSpecifier baseDamage)
{
HitEntities = hitEntities;
User = user;
BaseDamage = baseDamage;
}
}

View File

@@ -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<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
SubscribeLocalEvent<MeleeWeaponComponent, GetVerbsEvent<ExamineVerb>>(OnMeleeExaminableVerb);
}
private void OnMeleeExaminableVerb(EntityUid uid, MeleeWeaponComponent component, GetVerbsEvent<ExamineVerb> 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<DamageableComponent>(ev.Target) ||
!TryComp<TransformComponent>(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<EntityUid> { ev.Target.Value }, user, damage);
RaiseLocalEvent(component.Owner, hitEvent);
if (hitEvent.Handled)
return;
var targets = new List<EntityUid>(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<TransformComponent>(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<EntityUid>();
var damageQuery = GetEntityQuery<DamageableComponent>();
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<CombatModeComponent>(user, out var combatMode))
return false;
var target = ev.Target!.Value;
if (!TryComp<HandsComponent>(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<EntityUid>() {target}));
return true;
}
private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, SharedCombatModeComponent disarmerComp)
{
if (HasComp<DisarmProneComponent>(disarmer))
return 1.0f;
if (HasComp<DisarmProneComponent>(disarmed))
return 0.0f;
var contestResults = 1 - _contests.OverallStrengthContest(disarmer, disarmed);
float chance = (disarmerComp.BaseDisarmFailChance + contestResults);
if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
{
chance += malus.Malus;
}
return Math.Clamp(chance, 0f, 1f);
}
private HashSet<EntityUid> 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<EntityUid>();
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<MeleeSoundComponent>(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<BloodstreamComponent>();
var bloodQuery = GetEntityQuery<BloodstreamComponent>();
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);
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged.Components; namespace Content.Server.Weapons.Ranged.Components;
[RegisterComponent] [RegisterComponent]
public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {} public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Weapon.Ranged.Components namespace Content.Server.Weapons.Ranged.Components
{ {
[RegisterComponent] [RegisterComponent]
public sealed class ChemicalAmmoComponent : Component public sealed class ChemicalAmmoComponent : Component

View File

@@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Weapon.Ranged.Components; namespace Content.Server.Weapons.Ranged.Components;
/// <summary> /// <summary>
/// Plays the specified sound upon receiving damage of that type. /// Plays the specified sound upon receiving damage of that type.

View File

@@ -1,6 +1,6 @@
using Robust.Shared.Audio; using Robust.Shared.Audio;
namespace Content.Server.Weapon.Ranged.Components; namespace Content.Server.Weapons.Ranged.Components;
/// <summary> /// <summary>
/// Responsible for handling recharging a basic entity ammo provider over time. /// Responsible for handling recharging a basic entity ammo provider over time.

View File

@@ -1,10 +1,10 @@
using System.Linq; using System.Linq;
using Content.Server.Chemistry.EntitySystems; 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.Chemistry.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
namespace Content.Server.Weapon.Ranged.Systems namespace Content.Server.Weapons.Ranged.Systems
{ {
public sealed class ChemicalAmmoSystem : EntitySystem public sealed class ChemicalAmmoSystem : EntitySystem
{ {

View File

@@ -1,5 +1,5 @@
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {} public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map; using Robust.Shared.Map;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem public sealed partial class GunSystem
{ {

View File

@@ -7,7 +7,7 @@ using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem public sealed partial class GunSystem
{ {

View File

@@ -6,7 +6,7 @@ using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem public sealed partial class GunSystem
{ {

View File

@@ -1,6 +1,6 @@
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem public sealed partial class GunSystem
{ {

View File

@@ -6,15 +6,12 @@ using Content.Server.Interaction;
using Content.Server.Interaction.Components; using Content.Server.Interaction.Components;
using Content.Server.Projectiles.Components; using Content.Server.Projectiles.Components;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Weapon.Melee; using Content.Server.Weapons.Melee;
using Content.Server.Weapon.Ranged.Components; using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Audio;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.StatusEffect;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Vehicle.Components;
using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
@@ -28,7 +25,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem; 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 public sealed partial class GunSystem : SharedGunSystem
{ {
@@ -196,7 +193,7 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
if (dmg.Total > FixedPoint2.Zero) if (dmg.Total > FixedPoint2.Zero)
{ {
RaiseNetworkEvent(new DamageEffectEvent(hitEntity), Filter.Pvs(hitEntity, entityManager: EntityManager)); RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {result.HitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager));
} }
PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);

View File

@@ -1,13 +1,12 @@
using Content.Server.Weapon.Ranged.Components; using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; 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 public sealed class RechargeBasicEntityAmmoSystem : EntitySystem
{ {

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Server.Ghost.Components; using Content.Server.Ghost.Components;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
@@ -14,7 +13,7 @@ using Robust.Shared.Players;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Systems; namespace Content.Server.Weapons.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem public sealed class TetherGunSystem : SharedTetherGunSystem
{ {

View File

@@ -1,10 +1,10 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Weapon.Ranged.Systems; using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Server.Weapons.Ranged.Commands; namespace Content.Server.Weapons;
[AdminCommand(AdminFlags.Fun)] [AdminCommand(AdminFlags.Fun)]
public sealed class TetherGunCommand : IConsoleCommand public sealed class TetherGunCommand : IConsoleCommand

View File

@@ -1,7 +1,6 @@
using Content.Server.DoAfter; using Content.Server.DoAfter;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
using Content.Server.Hands.Systems; using Content.Server.Hands.Systems;
using Content.Server.Weapon.Melee;
using Content.Server.Wieldable.Components; using Content.Server.Wieldable.Components;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
@@ -12,6 +11,7 @@ using Content.Shared.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Player; using Robust.Shared.Player;
using Content.Server.Actions.Events; using Content.Server.Actions.Events;
using Content.Server.Weapons.Melee.Events;
namespace Content.Server.Wieldable namespace Content.Server.Wieldable

View File

@@ -3,6 +3,7 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;

View File

@@ -2,6 +2,7 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Physics.Pull; using Content.Shared.Physics.Pull;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;

View File

@@ -3,7 +3,6 @@ using Robust.Shared.Random;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Disease.Components; using Content.Server.Disease.Components;
using Content.Server.Drone.Components; using Content.Server.Drone.Components;
using Content.Server.Weapon.Melee;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Server.Disease; using Content.Server.Disease;
@@ -13,6 +12,8 @@ using Content.Server.Inventory;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Server.Speech; using Content.Server.Speech;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Weapons.Melee.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Zombies; using Content.Shared.Zombies;

View File

@@ -16,7 +16,6 @@ using Content.Server.Ghost.Roles.Components;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
using Content.Server.Mind.Commands; using Content.Server.Mind.Commands;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.MobState; using Content.Shared.MobState;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -29,6 +28,7 @@ using Content.Server.Humanoid;
using Content.Server.IdentityManagement; using Content.Server.IdentityManagement;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Audio; using Robust.Shared.Audio;
namespace Content.Server.Zombies 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 //This is the actual damage of the zombie. We assign the visual appearance
//and range here because of stuff we'll find out later //and range here because of stuff we'll find out later
var melee = EnsureComp<MeleeWeaponComponent>(target); var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.Arc = zombiecomp.AttackArc; melee.Animation = zombiecomp.AttackAnimation;
melee.ClickArc = zombiecomp.AttackArc;
melee.Range = 0.75f; melee.Range = 0.75f;
//We have specific stuff for humanoid zombies because they matter more //We have specific stuff for humanoid zombies because they matter more

View File

@@ -15,16 +15,5 @@ namespace Content.Shared.CombatMode
public TargetingZone TargetZone { get; } public TargetingZone TargetZone { get; }
} }
[Serializable, NetSerializable]
public sealed class SetCombatModeActiveMessage : EntityEventArgs
{
public SetCombatModeActiveMessage(bool active)
{
Active = active;
}
public bool Active { get; }
}
} }
} }

View File

@@ -0,0 +1,7 @@
using Content.Shared.Actions;
namespace Content.Shared.CombatMode;
public sealed class TogglePrecisionModeEvent : InstantActionEvent
{
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.CombatMode;
using Content.Shared.Actions; using Content.Shared.Actions;
namespace Content.Shared.CombatMode.Pacification namespace Content.Shared.CombatMode.Pacification
@@ -18,11 +17,9 @@ namespace Content.Shared.CombatMode.Pacification
if (!TryComp<SharedCombatModeComponent>(uid, out var combatMode)) if (!TryComp<SharedCombatModeComponent>(uid, out var combatMode))
return; return;
if (combatMode.DisarmAction != null) if (combatMode.CanDisarm != null)
{ combatMode.CanDisarm = false;
_actionsSystem.SetToggled(combatMode.DisarmAction, false);
_actionsSystem.SetEnabled(combatMode.DisarmAction, false);
}
if (combatMode.CombatToggleAction != null) if (combatMode.CombatToggleAction != null)
{ {
combatMode.IsInCombatMode = false; combatMode.IsInCombatMode = false;
@@ -35,8 +32,8 @@ namespace Content.Shared.CombatMode.Pacification
if (!TryComp<SharedCombatModeComponent>(uid, out var combatMode)) if (!TryComp<SharedCombatModeComponent>(uid, out var combatMode))
return; return;
if (combatMode.DisarmAction != null) if (combatMode.CanDisarm != null)
_actionsSystem.SetEnabled(combatMode.DisarmAction, true); combatMode.CanDisarm = true;
if (combatMode.CombatToggleAction != null) if (combatMode.CombatToggleAction != null)
_actionsSystem.SetEnabled(combatMode.CombatToggleAction, true); _actionsSystem.SetEnabled(combatMode.CombatToggleAction, true);
} }

View File

@@ -5,17 +5,21 @@ using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.CombatMode namespace Content.Shared.CombatMode
{ {
[NetworkedComponent()] [NetworkedComponent()]
public abstract class SharedCombatModeComponent : Component public abstract class SharedCombatModeComponent : Component
{ {
private bool _isInCombatMode; #region Disarm
private TargetingZone _activeZone;
[DataField("disarmFailChance")] /// <summary>
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.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("disarm")]
public bool? CanDisarm;
[DataField("disarmFailSound")] [DataField("disarmFailSound")]
public readonly SoundSpecifier DisarmFailSound = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg"); public readonly SoundSpecifier DisarmFailSound = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg");
@@ -23,14 +27,13 @@ namespace Content.Shared.CombatMode
[DataField("disarmSuccessSound")] [DataField("disarmSuccessSound")]
public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
[DataField("disarmActionId", customTypeSerializer:typeof(PrototypeIdSerializer<EntityTargetActionPrototype>))] [DataField("disarmFailChance")]
public readonly string DisarmActionId = "Disarm"; public readonly float BaseDisarmFailChance = 0.75f;
[DataField("canDisarm")] #endregion
public bool CanDisarm;
[DataField("disarmAction")] // must be a data-field to properly save cooldown when saving game state. private bool _isInCombatMode;
public EntityTargetAction? DisarmAction; private TargetingZone _activeZone;
[DataField("combatToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))] [DataField("combatToggleActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public readonly string CombatToggleActionId = "CombatModeToggle"; public readonly string CombatToggleActionId = "CombatModeToggle";
@@ -49,19 +52,6 @@ namespace Content.Shared.CombatMode
if (CombatToggleAction != null) if (CombatToggleAction != null)
EntitySystem.Get<SharedActionsSystem>().SetToggled(CombatToggleAction, _isInCombatMode); EntitySystem.Get<SharedActionsSystem>().SetToggled(CombatToggleAction, _isInCombatMode);
Dirty(); 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(); 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;
}
}
} }
} }

View File

@@ -1,21 +1,20 @@
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes; using Content.Shared.Actions.ActionTypes;
using Content.Shared.Targeting;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.CombatMode namespace Content.Shared.CombatMode
{ {
public abstract class SharedCombatModeSystem : EntitySystem public abstract class SharedCombatModeSystem : EntitySystem
{ {
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeNetworkEvent<CombatModeSystemMessages.SetCombatModeActiveMessage>(CombatModeActiveHandler);
SubscribeLocalEvent<CombatModeSystemMessages.SetCombatModeActiveMessage>(CombatModeActiveHandler);
SubscribeLocalEvent<SharedCombatModeComponent, ComponentStartup>(OnStartup); SubscribeLocalEvent<SharedCombatModeComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<SharedCombatModeComponent, ComponentShutdown>(OnShutdown); SubscribeLocalEvent<SharedCombatModeComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SharedCombatModeComponent, ToggleCombatActionEvent>(OnActionPerform); SubscribeLocalEvent<SharedCombatModeComponent, ToggleCombatActionEvent>(OnActionPerform);
@@ -31,30 +30,17 @@ namespace Content.Shared.CombatMode
if (component.CombatToggleAction != null) if (component.CombatToggleAction != null)
_actionsSystem.AddAction(uid, 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) private void OnShutdown(EntityUid uid, SharedCombatModeComponent component, ComponentShutdown args)
{ {
if (component.CombatToggleAction != null) if (component.CombatToggleAction != null)
_actionsSystem.RemoveAction(uid, component.CombatToggleAction); _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<SharedCombatModeComponent>(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) private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
@@ -66,17 +52,19 @@ namespace Content.Shared.CombatMode
args.Handled = true; 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)) public CombatModeComponentState(bool isInCombatMode, TargetingZone targetingZone)
return; {
IsInCombatMode = isInCombatMode;
combatModeComponent.IsInCombatMode = ev.Active; TargetingZone = targetingZone;
}
} }
} }
public sealed class ToggleCombatActionEvent : InstantActionEvent { } public sealed class ToggleCombatActionEvent : InstantActionEvent { }
public sealed class DisarmActionEvent : EntityTargetActionEvent { }
} }

View File

@@ -22,7 +22,6 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward"; public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward";
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward"; public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu"; public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
public static readonly BoundKeyFunction OpenContextMenu = "OpenContextMenu";
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu"; public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu";
public static readonly BoundKeyFunction SmartEquipBackpack = "SmartEquipBackpack"; public static readonly BoundKeyFunction SmartEquipBackpack = "SmartEquipBackpack";

View File

@@ -198,12 +198,9 @@ namespace Content.Shared.Interaction
if (target != null && Deleted(target.Value)) if (target != null && Deleted(target.Value))
return; return;
// TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? if (TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode)
if (!altInteract && 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. // Eat the input
var shouldWideAttack = target == null || !InRangeUnobstructed(user, target.Value);
DoAttack(user, coordinates, shouldWideAttack, target);
return; return;
} }
@@ -300,12 +297,6 @@ namespace Content.Shared.Interaction
checkAccess: false); 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, public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
EntityCoordinates clickLocation, bool inRangeUnobstructed) EntityCoordinates clickLocation, bool inRangeUnobstructed)
{ {

View File

@@ -1,3 +1,4 @@
using Content.Shared.CombatMode;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
@@ -9,8 +10,9 @@ namespace Content.Shared.Item;
public abstract class SharedItemSystem : EntitySystem public abstract class SharedItemSystem : EntitySystem
{ {
[Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] protected readonly SharedContainerSystem Container = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -69,7 +71,7 @@ public abstract class SharedItemSystem : EntitySystem
private void OnHandInteract(EntityUid uid, ItemComponent component, InteractHandEvent args) private void OnHandInteract(EntityUid uid, ItemComponent component, InteractHandEvent args)
{ {
if (args.Handled) if (args.Handled || _combatMode.IsInCombatMode(args.User))
return; return;
args.Handled = _handsSystem.TryPickup(args.User, uid, animateUser: false); 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. // 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 // this occurs when the item is in their inventory or in an open backpack
_container.TryGetContainingContainer(args.User, out var userContainer); Container.TryGetContainingContainer(args.User, out var userContainer);
if (_container.TryGetContainingContainer(args.Target, out var container) && container != userContainer) if (Container.TryGetContainingContainer(args.Target, out var container) && container != userContainer)
verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory"); verb.Text = Loc.GetString("pick-up-verb-get-data-text-inventory");
else else
verb.Text = Loc.GetString("pick-up-verb-get-data-text"); verb.Text = Loc.GetString("pick-up-verb-get-data-text");

View File

@@ -29,7 +29,6 @@ public sealed class ThrowingSystem : EntitySystem
/// <param name="uid">The entity being thrown.</param> /// <param name="uid">The entity being thrown.</param>
/// <param name="direction">A vector pointing from the entity to its destination.</param> /// <param name="direction">A vector pointing from the entity to its destination.</param>
/// <param name="strength">How much the direction vector should be multiplied for velocity.</param> /// <param name="strength">How much the direction vector should be multiplied for velocity.</param>
/// <param name="user"></param>
/// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param> /// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param>
public void TryThrow( public void TryThrow(
EntityUid uid, EntityUid uid,

View File

@@ -1,94 +0,0 @@
using Robust.Shared.Map;
namespace Content.Shared.Weapons.Melee
{
/// <summary>
/// Raised directed on the used entity when a target entity is click attacked by a user.
/// </summary>
public sealed class ClickAttackEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity used to attack, for broadcast purposes.
/// </summary>
public EntityUid Used { get; }
/// <summary>
/// Entity that triggered the attack.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
/// <summary>
/// The entity that was attacked.
/// </summary>
public EntityUid? Target { get; }
public ClickAttackEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation, EntityUid? target = null)
{
Used = used;
User = user;
ClickLocation = clickLocation;
Target = target;
}
}
/// <summary>
/// Raised directed on the used entity when a target entity is wide attacked by a user.
/// </summary>
public sealed class WideAttackEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity used to attack, for broadcast purposes.
/// </summary>
public EntityUid Used { get; }
/// <summary>
/// Entity that triggered the attack.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
public WideAttackEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation)
{
Used = used;
User = user;
ClickLocation = clickLocation;
}
}
/// <summary>
/// Event raised on entities that have been attacked.
/// </summary>
public sealed class AttackedEvent : EntityEventArgs
{
/// <summary>
/// Entity used to attack, for broadcast purposes.
/// </summary>
public EntityUid Used { get; }
/// <summary>
/// Entity that triggered the attack.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
public AttackedEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation)
{
Used = used;
User = user;
ClickLocation = clickLocation;
}
}
}

View File

@@ -8,10 +8,16 @@ namespace Content.Shared.Weapons.Melee;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class DamageEffectEvent : EntityEventArgs public sealed class DamageEffectEvent : EntityEventArgs
{ {
public EntityUid Entity; /// <summary>
/// Color to play for the damage flash.
/// </summary>
public Color Color;
public DamageEffectEvent(EntityUid entity) public List<EntityUid> Entities;
public DamageEffectEvent(Color color, List<EntityUid> entities)
{ {
Entity = entity; Color = color;
Entities = entities;
} }
} }

View File

@@ -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
{
/// <summary>
/// Coordinates being attacked.
/// </summary>
public readonly EntityCoordinates Coordinates;
protected AttackEvent(EntityCoordinates coordinates)
{
Coordinates = coordinates;
}
}
/// <summary>
/// Event raised on entities that have been attacked.
/// </summary>
public sealed class AttackedEvent : EntityEventArgs
{
/// <summary>
/// Entity used to attack, for broadcast purposes.
/// </summary>
public EntityUid Used { get; }
/// <summary>
/// Entity that triggered the attack.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
public AttackedEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation)
{
Used = used;
User = user;
ClickLocation = clickLocation;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Melee.Events;
/// <summary>
/// Raised on the client when it attempts a heavy attack.
/// </summary>
[Serializable, NetSerializable]
public sealed class HeavyAttackEvent : AttackEvent
{
public readonly EntityUid Weapon;
public HeavyAttackEvent(EntityUid weapon, EntityCoordinates coordinates) : base(coordinates)
{
Weapon = weapon;
}
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Melee.Events;
/// <summary>
/// Raised when a light attack is made.
/// </summary>
[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;
}
}

View File

@@ -0,0 +1,35 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Melee.Events;
/// <summary>
/// Data for melee lunges from attacks.
/// </summary>
[Serializable, NetSerializable]
public sealed class MeleeLungeEvent : EntityEventArgs
{
public EntityUid Entity;
/// <summary>
/// Width of the attack angle.
/// </summary>
public Angle Angle;
/// <summary>
/// The relative local position to the <see cref="Entity"/>
/// </summary>
public Vector2 LocalPos;
/// <summary>
/// Entity to spawn for the animation
/// </summary>
public string? Animation;
public MeleeLungeEvent(EntityUid uid, Angle angle, Vector2 localPos, string? animation)
{
Entity = uid;
Angle = angle;
LocalPos = localPos;
Animation = animation;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Melee.Events;
/// <summary>
/// Raised by the client if it pre-emptively stops a heavy attack.
/// </summary>
[Serializable, NetSerializable]
public sealed class StopHeavyAttackEvent : EntityEventArgs
{
public readonly EntityUid Weapon;
public StopHeavyAttackEvent(EntityUid weapon)
{
Weapon = weapon;
}
}

View File

@@ -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<EntityPrototype>))]
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,
}
}

View File

@@ -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;
/// <summary>
/// When given to a mob lets them do unarmed attacks, or when given to an item lets someone wield it to do attacks.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class MeleeWeaponComponent : Component
{
/// <summary>
/// Should the melee weapon's damage stats be examinable.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("hidden")]
public bool HideFromExamine { get; set; } = false;
/// <summary>
/// Next time this component is allowed to light attack. Heavy attacks are wound up and never have a cooldown.
/// </summary>
[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.
*/
/// <summary>
/// How many times we can attack per second.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("attackRate")]
public float AttackRate = 1f;
/// <summary>
/// Are we currently holding down the mouse for an attack.
/// Used so we can't just hold the mouse button and attack constantly.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Attacking = false;
/// <summary>
/// When did we start a heavy attack.
/// </summary>
/// <returns></returns>
[ViewVariables(VVAccess.ReadWrite), DataField("windUpStart")]
public TimeSpan? WindUpStart;
/// <summary>
/// How long it takes a heavy attack to windup.
/// </summary>
[ViewVariables]
public TimeSpan WindupTime => AttackRate > 0 ? TimeSpan.FromSeconds(1 / AttackRate * HeavyWindupModifier) : TimeSpan.Zero;
/// <summary>
/// Heavy attack windup time gets multiplied by this value and the light attack cooldown.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("heavyWindupModifier")]
public float HeavyWindupModifier = 1.5f;
/// <summary>
/// Light attacks get multiplied by this over the base <see cref="Damage"/> value.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("heavyDamageModifier")]
public FixedPoint2 HeavyDamageModifier = FixedPoint2.New(2);
/// <summary>
/// Base damage for this weapon. Can be modified via heavy damage or other means.
/// </summary>
[DataField("damage", required:true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
[DataField("bluntStaminaDamageFactor")]
[ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 BluntStaminaDamageFactor { get; set; } = 0.5f;
/// <summary>
/// Nearest edge range to hit an entity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("range")]
public float Range = 1f;
/// <summary>
/// Total width of the angle for wide attacks.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("angle")]
public Angle Angle = Angle.FromDegrees(60);
[ViewVariables(VVAccess.ReadWrite), DataField("animation", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Animation = "WeaponArcSlash";
// Sounds
/// <summary>
/// This gets played whenever a melee attack is done. This is predicted by the client.
/// </summary>
[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;
/// <summary>
/// Plays if no damage is done to the target entity.
/// </summary>
[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;
}
}

View File

@@ -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<EntityUid> 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<EntityUid> 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;
}
}
}
}

View File

@@ -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!;
/// <summary>
/// If an attack is released within this buffer it's assumed to be full damage.
/// </summary>
public const float GracePeriod = 0.05f;
public override void Initialize()
{
base.Initialize();
Sawmill = Logger.GetSawmill("melee");
SubscribeLocalEvent<MeleeWeaponComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<MeleeWeaponComponent, ComponentHandleState>(OnHandleState);
SubscribeAllEvent<LightAttackEvent>(OnLightAttack);
SubscribeAllEvent<StartHeavyAttackEvent>(OnStartHeavyAttack);
SubscribeAllEvent<StopHeavyAttackEvent>(OnStopHeavyAttack);
SubscribeAllEvent<HeavyAttackEvent>(OnHeavyAttack);
SubscribeAllEvent<DisarmAttackEvent>(OnDisarmAttack);
SubscribeAllEvent<StopAttackEvent>(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<MeleeWeaponComponent>(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<MeleeWeaponComponent>(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<TransformComponent>(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<TransformComponent>(target, out var targetXform))
return;
AttemptAttack(user, weapon, new DisarmAttackEvent(target, targetXform.Coordinates));
}
/// <summary>
/// Called when a windup is finished and an attack is tried.
/// </summary>
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);
}
/// <summary>
/// When an attack is released get the actual modifier for damage done.
/// </summary>
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<TransformComponent>(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);
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Weapons.Melee;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Zombies namespace Content.Shared.Zombies
@@ -52,8 +52,8 @@ namespace Content.Shared.Zombies
/// <summary> /// <summary>
/// The attack arc of the zombie /// The attack arc of the zombie
/// </summary> /// </summary>
[DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer<MeleeWeaponAnimationPrototype>))] [DataField("attackArc", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string AttackArc = "claw"; public string AttackAnimation = "WeaponArcClaw";
/// <summary> /// <summary>
/// The role prototype of the zombie antag role /// The role prototype of the zombie antag role

View File

@@ -1,6 +1,12 @@
action-name-combat = [color=red]Combat Mode[/color] 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-name-precision = [color=red]Precision mode[/color]
action-popup-combat-enabled = Combat mode enabled! 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

View File

@@ -1,7 +1,6 @@
disarm-action-free-hand = You need to use a free hand to disarm! disarm-action-disarmable = {THE($targetName)} is not disarmable!
disarm-action-popup-message-other-clients = {CAPITALIZE(THE($performerName))} disarmed {THE($targetName)}!
disarm-action-popup-message-other-clients = {CAPITALIZE(THE($performerName))} fails to disarm {THE($targetName)}! disarm-action-popup-message-cursor = Disarmed {THE($targetName)}!
disarm-action-popup-message-cursor = You fail to disarm {THE($targetName)}!
action-name-disarm = [color=red]Disarm[/color] action-name-disarm = [color=red]Disarm[/color]
action-description-disarm = Attempt to [color=red]disarm[/color] someone. action-description-disarm = Attempt to [color=red]disarm[/color] someone.

View File

@@ -91,6 +91,7 @@ ui-options-function-camera-rotate-right = Rotate right
ui-options-function-camera-reset = Reset ui-options-function-camera-reset = Reset
ui-options-function-use = Use ui-options-function-use = Use
ui-options-function-alt-use = Alt use
ui-options-function-wide-attack = Wide attack ui-options-function-wide-attack = Wide attack
ui-options-function-activate-item-in-hand = Activate item in hand ui-options-function-activate-item-in-hand = Activate item in hand
ui-options-function-alt-activate-item-in-hand = Alternative 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 ## Network menu
ui-options-net-interp-ratio = State buffer size ui-options-net-interp-ratio = State buffer size
ui-options-net-interp-ratio-tooltip = Increasing this will generally make the game more resistant 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 to server->client packet-loss, however in doing so it
effectively adds slightly more latency and requires the effectively adds slightly more latency and requires the
client to predict more future ticks. client to predict more future ticks.
ui-options-net-predict-tick-bias = Prediction tick bias 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 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 to client->server packet-loss, however in doing so it
effectively adds slightly more latency and requires the effectively adds slightly more latency and requires the
client to predict more future ticks. client to predict more future ticks.
ui-options-net-pvs-entry = PVS entity budget 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 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 entities to the client. Lowering this can help reduce
stuttering due to entity spawning, but can lead to pop-in. stuttering due to entity spawning, but can lead to pop-in.
ui-options-net-pvs-leave = PVS detach rate ui-options-net-pvs-leave = PVS detach rate
ui-options-net-pvs-leave-tooltip = This limits the rate at which the client will remove 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 out-of-view entities. Lowering this can help reduce
stuttering when walking around, but could occasionally stuttering when walking around, but could occasionally
lead to mispredicts and other issues. lead to mispredicts and other issues.

View File

@@ -30,22 +30,6 @@
useDelay: 1 # equip noise spam. useDelay: 1 # equip noise spam.
event: !type:ToggleClothingEvent 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 - type: instantAction
id: CombatModeToggle id: CombatModeToggle
name: action-name-combat name: action-name-combat

View File

@@ -20,9 +20,10 @@
- type: Clothing - type: Clothing
sprite: Clothing/Eyes/Glasses/gar.rsi sprite: Clothing/Eyes/Glasses/gar.rsi
- type: MeleeWeapon - type: MeleeWeapon
attackRate: 1.5
damage: damage:
types: types:
Blunt: 10 Blunt: 7
- type: entity - type: entity
parent: ClothingEyesBase parent: ClothingEyesBase

View File

@@ -11,14 +11,16 @@
- type: BoxingGloves - type: BoxingGloves
- type: StaminaDamageOnHit - type: StaminaDamageOnHit
damage: 8 #Stam damage values seem a bit higher than regular damage because of the decay, etc 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 - type: MeleeWeapon
attackRate: 1.5
damage: damage:
types: types:
Blunt: 0.6 Blunt: 0.4
hitSound: soundHit:
collection: BoxingHit collection: BoxingHit
arc: fist animation: WeaponArcFist
- type: Fiber - type: Fiber
fiberMaterial: fibers-leather fiberMaterial: fibers-leather
fiberColor: fibers-red fiberColor: fibers-red

Some files were not shown because too many files have changed in this diff Show More