2023-03-12 15:56:05 +11:00
using System.Diagnostics.CodeAnalysis ;
2022-11-14 08:33:54 +11:00
using System.Linq ;
2023-07-08 14:08:32 +10:00
using System.Numerics ;
2022-09-29 15:51:59 +10:00
using Content.Shared.ActionBlocker ;
2022-11-14 08:33:54 +11:00
using Content.Shared.Administration.Logs ;
2022-09-29 15:51:59 +10:00
using Content.Shared.CombatMode ;
2022-11-14 08:33:54 +11:00
using Content.Shared.Damage ;
using Content.Shared.Damage.Systems ;
using Content.Shared.Database ;
using Content.Shared.FixedPoint ;
2022-10-04 12:12:45 +11:00
using Content.Shared.Hands ;
2022-09-29 15:51:59 +10:00
using Content.Shared.Hands.Components ;
2022-11-14 08:33:54 +11:00
using Content.Shared.Interaction ;
2022-10-16 07:20:05 +11:00
using Content.Shared.Inventory ;
2022-11-14 08:33:54 +11:00
using Content.Shared.Physics ;
2022-09-29 15:51:59 +10:00
using Content.Shared.Popups ;
2022-11-14 08:33:54 +11:00
using Content.Shared.Weapons.Melee.Components ;
2022-09-29 15:51:59 +10:00
using Content.Shared.Weapons.Melee.Events ;
2023-05-14 13:15:18 +10:00
using Content.Shared.Weapons.Ranged.Components ;
2023-06-08 02:15:39 +00:00
using Content.Shared.Weapons.Ranged.Events ;
2023-05-14 13:15:18 +10:00
using Content.Shared.Weapons.Ranged.Systems ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Audio ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2022-09-29 15:51:59 +10:00
using Robust.Shared.Map ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Physics ;
using Robust.Shared.Physics.Systems ;
2023-10-28 09:59:53 +11:00
using Robust.Shared.Player ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Prototypes ;
2022-09-29 15:51:59 +10:00
using Robust.Shared.Timing ;
namespace Content.Shared.Weapons.Melee ;
public abstract class SharedMeleeWeaponSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default ! ;
[Dependency] protected readonly IMapManager MapManager = default ! ;
2022-11-14 08:33:54 +11:00
[Dependency] private readonly IPrototypeManager _protoManager = default ! ;
[Dependency] protected readonly ISharedAdminLogManager AdminLogger = default ! ;
2022-09-29 15:51:59 +10:00
[Dependency] protected readonly ActionBlockerSystem Blocker = default ! ;
2022-11-14 08:33:54 +11:00
[Dependency] protected readonly DamageableSystem Damageable = default ! ;
2023-03-12 15:56:05 +11:00
[Dependency] private readonly InventorySystem _inventory = default ! ;
2022-09-29 15:51:59 +10:00
[Dependency] protected readonly SharedAudioSystem Audio = default ! ;
[Dependency] protected readonly SharedCombatModeSystem CombatMode = default ! ;
2022-11-14 08:33:54 +11:00
[Dependency] protected readonly SharedInteractionSystem Interaction = default ! ;
[Dependency] private readonly SharedPhysicsSystem _physics = default ! ;
2022-09-29 15:51:59 +10:00
[Dependency] protected readonly SharedPopupSystem PopupSystem = default ! ;
2023-05-08 17:46:26 +10:00
[Dependency] protected readonly SharedTransformSystem TransformSystem = default ! ;
2022-11-14 08:33:54 +11:00
[Dependency] private readonly StaminaSystem _stamina = default ! ;
2022-09-29 15:51:59 +10:00
2022-11-14 08:33:54 +11:00
public const float DamagePitchVariation = 0.05f ;
private const int AttackMask = ( int ) ( CollisionGroup . MobMask | CollisionGroup . Opaque ) ;
2023-05-08 17:46:26 +10:00
/// <summary>
/// Maximum amount of targets allowed for a wide-attack.
/// </summary>
public const int MaxTargets = 5 ;
2022-09-29 15:51:59 +10:00
/// <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 ( ) ;
2023-05-01 14:49:25 +10:00
SubscribeLocalEvent < MeleeWeaponComponent , EntityUnpausedEvent > ( OnMeleeUnpaused ) ;
2022-11-04 12:18:00 +11:00
SubscribeLocalEvent < MeleeWeaponComponent , HandSelectedEvent > ( OnMeleeSelected ) ;
2023-06-08 02:15:39 +00:00
SubscribeLocalEvent < MeleeWeaponComponent , ShotAttemptedEvent > ( OnMeleeShotAttempted ) ;
2023-05-14 13:15:18 +10:00
SubscribeLocalEvent < MeleeWeaponComponent , GunShotEvent > ( OnMeleeShot ) ;
2023-05-28 03:03:25 -04:00
SubscribeLocalEvent < BonusMeleeDamageComponent , GetMeleeDamageEvent > ( OnGetBonusMeleeDamage ) ;
2023-06-07 16:26:45 -04:00
SubscribeLocalEvent < BonusMeleeDamageComponent , GetHeavyDamageModifierEvent > ( OnGetBonusHeavyDamageModifier ) ;
SubscribeLocalEvent < BonusMeleeAttackRateComponent , GetMeleeAttackRateEvent > ( OnGetBonusMeleeAttackRate ) ;
2022-09-29 15:51:59 +10:00
2023-05-02 05:07:17 +10:00
SubscribeAllEvent < HeavyAttackEvent > ( OnHeavyAttack ) ;
2022-09-29 15:51:59 +10:00
SubscribeAllEvent < LightAttackEvent > ( OnLightAttack ) ;
SubscribeAllEvent < DisarmAttackEvent > ( OnDisarmAttack ) ;
SubscribeAllEvent < StopAttackEvent > ( OnStopAttack ) ;
2023-04-06 11:37:50 +12:00
#if DEBUG
SubscribeLocalEvent < MeleeWeaponComponent , MapInitEvent > ( OnMapInit ) ;
}
private void OnMapInit ( EntityUid uid , MeleeWeaponComponent component , MapInitEvent args )
{
2023-05-01 14:49:25 +10:00
if ( component . NextAttack > Timing . CurTime )
2023-06-27 23:56:52 +10:00
Log . Warning ( $"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}" ) ;
2023-04-06 11:37:50 +12:00
#endif
2022-09-29 15:51:59 +10:00
}
2023-06-08 02:15:39 +00:00
private void OnMeleeShotAttempted ( EntityUid uid , MeleeWeaponComponent comp , ref ShotAttemptedEvent args )
{
if ( comp . NextAttack > Timing . CurTime )
args . Cancel ( ) ;
}
2023-05-14 13:15:18 +10:00
private void OnMeleeShot ( EntityUid uid , MeleeWeaponComponent component , ref GunShotEvent args )
{
if ( ! TryComp < GunComponent > ( uid , out var gun ) )
return ;
if ( gun . NextFire > component . NextAttack )
{
component . NextAttack = gun . NextFire ;
2023-09-06 18:30:23 +10:00
Dirty ( uid , component ) ;
2023-05-14 13:15:18 +10:00
}
}
2023-05-01 14:49:25 +10:00
private void OnMeleeUnpaused ( EntityUid uid , MeleeWeaponComponent component , ref EntityUnpausedEvent args )
{
component . NextAttack + = args . PausedTime ;
}
2022-11-04 12:18:00 +11:00
private void OnMeleeSelected ( EntityUid uid , MeleeWeaponComponent component , HandSelectedEvent args )
{
2023-06-07 16:26:45 -04:00
var attackRate = GetAttackRate ( uid , args . User , component ) ;
if ( attackRate . Equals ( 0f ) )
2022-11-04 12:18:00 +11:00
return ;
2023-01-27 03:04:26 +02:00
if ( ! component . ResetOnHandSelected )
return ;
2023-04-06 11:37:50 +12:00
if ( Paused ( uid ) )
return ;
2022-11-04 12:18:00 +11:00
// If someone swaps to this weapon then reset its cd.
var curTime = Timing . CurTime ;
2023-06-07 16:26:45 -04:00
var minimum = curTime + TimeSpan . FromSeconds ( 1 / attackRate ) ;
2022-11-04 12:18:00 +11:00
if ( minimum < component . NextAttack )
return ;
component . NextAttack = minimum ;
2023-09-06 18:30:23 +10:00
Dirty ( uid , component ) ;
2022-11-04 12:18:00 +11:00
}
2023-05-28 03:03:25 -04:00
private void OnGetBonusMeleeDamage ( EntityUid uid , BonusMeleeDamageComponent component , ref GetMeleeDamageEvent args )
{
2023-06-07 16:26:45 -04:00
if ( component . BonusDamage ! = null )
args . Damage + = component . BonusDamage ;
if ( component . DamageModifierSet ! = null )
args . Modifiers . Add ( component . DamageModifierSet ) ;
}
private void OnGetBonusHeavyDamageModifier ( EntityUid uid , BonusMeleeDamageComponent component , ref GetHeavyDamageModifierEvent args )
{
args . DamageModifier + = component . HeavyDamageFlatModifier ;
args . Multipliers * = component . HeavyDamageMultiplier ;
}
private void OnGetBonusMeleeAttackRate ( EntityUid uid , BonusMeleeAttackRateComponent component , ref GetMeleeAttackRateEvent args )
{
args . Rate + = component . FlatModifier ;
args . Multipliers * = component . Multiplier ;
}
2022-09-29 15:51:59 +10:00
private void OnStopAttack ( StopAttackEvent msg , EntitySessionEventArgs args )
{
var user = args . SenderSession . AttachedEntity ;
if ( user = = null )
return ;
2023-03-12 15:56:05 +11:00
if ( ! TryGetWeapon ( user . Value , out var weaponUid , out var weapon ) | |
2023-09-11 09:42:41 +10:00
weaponUid ! = GetEntity ( msg . Weapon ) )
2023-03-12 15:56:05 +11:00
{
2022-09-29 15:51:59 +10:00
return ;
2023-03-12 15:56:05 +11:00
}
2022-09-29 15:51:59 +10:00
if ( ! weapon . Attacking )
return ;
weapon . Attacking = false ;
2023-09-06 18:30:23 +10:00
Dirty ( weaponUid , weapon ) ;
2022-09-29 15:51:59 +10:00
}
private void OnLightAttack ( LightAttackEvent msg , EntitySessionEventArgs args )
{
var user = args . SenderSession . AttachedEntity ;
if ( user = = null )
return ;
2023-03-12 15:56:05 +11:00
if ( ! TryGetWeapon ( user . Value , out var weaponUid , out var weapon ) | |
2023-09-11 09:42:41 +10:00
weaponUid ! = GetEntity ( msg . Weapon ) )
2023-03-12 15:56:05 +11:00
{
2022-09-29 15:51:59 +10:00
return ;
2023-03-12 15:56:05 +11:00
}
2022-09-29 15:51:59 +10:00
2023-09-11 09:42:41 +10:00
AttemptAttack ( args . SenderSession . AttachedEntity ! . Value , weaponUid , weapon , msg , args . SenderSession ) ;
2022-09-29 15:51:59 +10:00
}
private void OnHeavyAttack ( HeavyAttackEvent msg , EntitySessionEventArgs args )
{
2023-03-12 15:56:05 +11:00
if ( args . SenderSession . AttachedEntity = = null )
2022-09-29 15:51:59 +10:00
{
return ;
}
2023-03-12 15:56:05 +11:00
if ( ! TryGetWeapon ( args . SenderSession . AttachedEntity . Value , out var weaponUid , out var weapon ) | |
2023-09-11 09:42:41 +10:00
weaponUid ! = GetEntity ( msg . Weapon ) )
2023-03-12 15:56:05 +11:00
{
2022-09-29 15:51:59 +10:00
return ;
2023-03-12 15:56:05 +11:00
}
2022-09-29 15:51:59 +10:00
2023-09-11 09:42:41 +10:00
AttemptAttack ( args . SenderSession . AttachedEntity . Value , weaponUid , weapon , msg , args . SenderSession ) ;
2022-09-29 15:51:59 +10:00
}
private void OnDisarmAttack ( DisarmAttackEvent msg , EntitySessionEventArgs args )
{
if ( args . SenderSession . AttachedEntity = = null )
{
return ;
}
2023-03-12 15:56:05 +11:00
if ( ! TryGetWeapon ( args . SenderSession . AttachedEntity . Value , out var weaponUid , out var weapon ) )
{
2022-09-29 15:51:59 +10:00
return ;
2023-03-12 15:56:05 +11:00
}
2022-09-29 15:51:59 +10:00
2023-03-12 15:56:05 +11:00
AttemptAttack ( args . SenderSession . AttachedEntity . Value , weaponUid , weapon , msg , args . SenderSession ) ;
2022-09-29 15:51:59 +10:00
}
2023-06-07 16:26:45 -04:00
/// <summary>
/// Gets the total damage a weapon does, including modifiers like wielding and enablind/disabling
/// </summary>
public DamageSpecifier GetDamage ( EntityUid uid , EntityUid user , MeleeWeaponComponent ? component = null )
2023-05-28 03:03:25 -04:00
{
if ( ! Resolve ( uid , ref component , false ) )
return new DamageSpecifier ( ) ;
2023-06-07 16:26:45 -04:00
var ev = new GetMeleeDamageEvent ( uid , new ( component . Damage ) , new ( ) , user ) ;
RaiseLocalEvent ( uid , ref ev ) ;
return DamageSpecifier . ApplyModifierSets ( ev . Damage , ev . Modifiers ) ;
}
public float GetAttackRate ( EntityUid uid , EntityUid user , MeleeWeaponComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) )
return 0 ;
var ev = new GetMeleeAttackRateEvent ( uid , component . AttackRate , 1 , user ) ;
RaiseLocalEvent ( uid , ref ev ) ;
return ev . Rate * ev . Multipliers ;
}
public FixedPoint2 GetHeavyDamageModifier ( EntityUid uid , EntityUid user , MeleeWeaponComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) )
return FixedPoint2 . Zero ;
2023-09-06 18:30:23 +10:00
var ev = new GetHeavyDamageModifierEvent ( uid , component . ClickDamageModifier , 1 , user ) ;
2023-06-07 16:26:45 -04:00
RaiseLocalEvent ( uid , ref ev ) ;
return ev . DamageModifier * ev . Multipliers ;
}
2023-03-12 15:56:05 +11:00
public bool TryGetWeapon ( EntityUid entity , out EntityUid weaponUid , [ NotNullWhen ( true ) ] out MeleeWeaponComponent ? melee )
2022-09-29 15:51:59 +10:00
{
2023-03-12 15:56:05 +11:00
weaponUid = default ;
melee = null ;
2022-09-29 15:51:59 +10:00
2022-12-10 12:05:39 -05:00
var ev = new GetMeleeWeaponEvent ( ) ;
RaiseLocalEvent ( entity , ev ) ;
if ( ev . Handled )
{
2023-03-12 15:56:05 +11:00
if ( TryComp ( ev . Weapon , out melee ) )
{
weaponUid = ev . Weapon . Value ;
return true ;
}
return false ;
2022-12-10 12:05:39 -05:00
}
2022-09-29 15:51:59 +10:00
// Use inhands entity if we got one.
2023-04-07 11:21:12 -07:00
if ( EntityManager . TryGetComponent ( entity , out HandsComponent ? hands ) & &
2022-09-29 15:51:59 +10:00
hands . ActiveHandEntity is { } held )
{
if ( EntityManager . TryGetComponent ( held , out melee ) )
{
2023-03-12 15:56:05 +11:00
weaponUid = held ;
return true ;
2022-09-29 15:51:59 +10:00
}
2023-03-12 15:56:05 +11:00
return false ;
2022-09-29 15:51:59 +10:00
}
2022-10-16 07:20:05 +11:00
// Use hands clothing if applicable.
2023-03-12 15:56:05 +11:00
if ( _inventory . TryGetSlotEntity ( entity , "gloves" , out var gloves ) & &
2022-10-16 07:20:05 +11:00
TryComp < MeleeWeaponComponent > ( gloves , out var glovesMelee ) )
{
2023-03-12 15:56:05 +11:00
weaponUid = gloves . Value ;
melee = glovesMelee ;
return true ;
2022-10-16 07:20:05 +11:00
}
// Use our own melee
2022-09-29 15:51:59 +10:00
if ( TryComp ( entity , out melee ) )
{
2023-03-12 15:56:05 +11:00
weaponUid = entity ;
return true ;
2022-09-29 15:51:59 +10:00
}
2023-03-12 15:56:05 +11:00
return false ;
2022-09-29 15:51:59 +10:00
}
2022-12-12 00:37:09 +11:00
2023-02-13 07:55:39 -05:00
public void AttemptLightAttackMiss ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , EntityCoordinates coordinates )
2022-12-12 00:37:09 +11:00
{
2023-09-11 09:42:41 +10:00
AttemptAttack ( user , weaponUid , weapon , new LightAttackEvent ( null , GetNetEntity ( weaponUid ) , GetNetCoordinates ( coordinates ) ) , null ) ;
2022-12-12 00:37:09 +11:00
}
2022-09-29 15:51:59 +10:00
2023-07-06 14:42:17 +10:00
public bool AttemptLightAttack ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , EntityUid target )
2022-09-29 15:51:59 +10:00
{
if ( ! TryComp < TransformComponent > ( target , out var targetXform ) )
2023-07-06 14:42:17 +10:00
return false ;
2022-09-29 15:51:59 +10:00
2023-09-11 09:42:41 +10:00
return AttemptAttack ( user , weaponUid , weapon , new LightAttackEvent ( GetNetEntity ( target ) , GetNetEntity ( weaponUid ) , GetNetCoordinates ( targetXform . Coordinates ) ) , null ) ;
2022-09-29 15:51:59 +10:00
}
2023-07-06 14:42:17 +10:00
public bool AttemptDisarmAttack ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , EntityUid target )
2022-09-29 15:51:59 +10:00
{
if ( ! TryComp < TransformComponent > ( target , out var targetXform ) )
2023-07-06 14:42:17 +10:00
return false ;
2022-09-29 15:51:59 +10:00
2023-09-11 09:42:41 +10:00
return AttemptAttack ( user , weaponUid , weapon , new DisarmAttackEvent ( GetNetEntity ( target ) , GetNetCoordinates ( targetXform . Coordinates ) ) , null ) ;
2022-09-29 15:51:59 +10:00
}
/// <summary>
/// Called when a windup is finished and an attack is tried.
/// </summary>
2023-07-06 14:42:17 +10:00
/// <returns>True if attack successful</returns>
private bool AttemptAttack ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , AttackEvent attack , ICommonSession ? session )
2022-09-29 15:51:59 +10:00
{
var curTime = Timing . CurTime ;
if ( weapon . NextAttack > curTime )
2023-07-06 14:42:17 +10:00
return false ;
2022-09-29 15:51:59 +10:00
2022-10-04 12:50:09 +11:00
if ( ! CombatMode . IsInCombatMode ( user ) )
2023-07-06 14:42:17 +10:00
return false ;
2022-10-04 12:50:09 +11:00
2022-12-17 14:47:15 +11:00
switch ( attack )
{
case LightAttackEvent light :
2023-09-11 09:42:41 +10:00
var lightTarget = GetEntity ( light . Target ) ;
if ( ! Blocker . CanAttack ( user , lightTarget ) )
2023-07-06 14:42:17 +10:00
return false ;
2023-07-07 18:45:37 +10:00
// Can't self-attack if you're the weapon
2023-09-11 09:42:41 +10:00
if ( weaponUid = = lightTarget )
2023-07-07 18:45:37 +10:00
return false ;
2022-12-17 14:47:15 +11:00
break ;
case DisarmAttackEvent disarm :
2023-09-11 09:42:41 +10:00
var disarmTarget = GetEntity ( disarm . Target ) ;
if ( ! Blocker . CanAttack ( user , disarmTarget ) )
2023-07-06 14:42:17 +10:00
return false ;
2022-12-17 14:47:15 +11:00
break ;
default :
if ( ! Blocker . CanAttack ( user ) )
2023-07-06 14:42:17 +10:00
return false ;
2022-12-17 14:47:15 +11:00
break ;
}
2022-09-29 15:51:59 +10:00
// Windup time checked elsewhere.
2023-06-07 16:26:45 -04:00
var fireRate = TimeSpan . FromSeconds ( 1f / GetAttackRate ( weaponUid , user , weapon ) ) ;
2023-05-14 13:15:18 +10:00
var swings = 0 ;
2022-09-29 15:51:59 +10:00
2023-05-14 13:15:18 +10:00
// TODO: If we get autoattacks then probably need a shotcounter like guns so we can do timing properly.
2022-09-29 15:51:59 +10:00
if ( weapon . NextAttack < curTime )
weapon . NextAttack = curTime ;
2023-05-14 13:15:18 +10:00
while ( weapon . NextAttack < = curTime )
{
weapon . NextAttack + = fireRate ;
swings + + ;
}
2022-09-29 15:51:59 +10:00
2023-09-06 18:30:23 +10:00
Dirty ( weaponUid , weapon ) ;
2022-09-29 15:51:59 +10:00
2023-05-14 13:15:18 +10:00
// Do this AFTER attack so it doesn't spam every tick
var ev = new AttemptMeleeEvent ( ) ;
RaiseLocalEvent ( weaponUid , ref ev ) ;
if ( ev . Cancelled )
2022-09-29 15:51:59 +10:00
{
2023-05-14 13:15:18 +10:00
if ( ev . Message ! = null )
{
PopupSystem . PopupClient ( ev . Message , weaponUid , user ) ;
}
2022-10-15 15:14:07 +11:00
2023-07-06 14:42:17 +10:00
return false ;
2023-05-14 13:15:18 +10:00
}
// Attack confirmed
for ( var i = 0 ; i < swings ; i + + )
{
string animation ;
switch ( attack )
{
case LightAttackEvent light :
DoLightAttack ( user , light , weaponUid , weapon , session ) ;
2023-09-28 16:20:29 -07:00
animation = weapon . Animation ;
2023-05-14 13:15:18 +10:00
break ;
case DisarmAttackEvent disarm :
if ( ! DoDisarm ( user , disarm , weaponUid , weapon , session ) )
2023-07-06 14:42:17 +10:00
return false ;
2023-05-14 13:15:18 +10:00
2023-09-28 16:20:29 -07:00
animation = weapon . Animation ;
2023-05-14 13:15:18 +10:00
break ;
case HeavyAttackEvent heavy :
2023-08-06 12:55:38 +10:00
if ( ! DoHeavyAttack ( user , heavy , weaponUid , weapon , session ) )
return false ;
2023-05-14 13:15:18 +10:00
animation = weapon . WideAnimation ;
break ;
default :
throw new NotImplementedException ( ) ;
}
2023-10-17 20:12:00 -05:00
DoLungeAnimation ( user , weaponUid , weapon . Angle , GetCoordinates ( attack . Coordinates ) . ToMap ( EntityManager , TransformSystem ) , weapon . Range , animation ) ;
2022-09-29 15:51:59 +10:00
}
2023-10-11 03:55:53 +01:00
var attackEv = new MeleeAttackEvent ( weaponUid ) ;
RaiseLocalEvent ( user , ref attackEv ) ;
2022-09-29 15:51:59 +10:00
weapon . Attacking = true ;
2023-07-06 14:42:17 +10:00
return true ;
2022-09-29 15:51:59 +10:00
}
2022-11-14 08:33:54 +11:00
protected abstract bool InRange ( EntityUid user , EntityUid target , float range , ICommonSession ? session ) ;
2023-02-13 07:55:39 -05:00
protected virtual void DoLightAttack ( EntityUid user , LightAttackEvent ev , EntityUid meleeUid , MeleeWeaponComponent component , ICommonSession ? session )
2022-09-29 15:51:59 +10:00
{
2023-08-06 16:01:44 +00:00
// If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant
var damage = GetDamage ( meleeUid , user , component ) * GetHeavyDamageModifier ( meleeUid , user , component ) ;
2023-09-11 09:42:41 +10:00
var target = GetEntity ( ev . Target ) ;
2023-02-13 07:55:39 -05:00
// For consistency with wide attacks stuff needs damageable.
2023-09-11 09:42:41 +10:00
if ( Deleted ( target ) | |
! HasComp < DamageableComponent > ( target ) | |
! TryComp < TransformComponent > ( target , out var targetXform ) | |
2023-02-13 07:55:39 -05:00
// Not in LOS.
2023-09-11 09:42:41 +10:00
! InRange ( user , target . Value , component . Range , session ) )
2022-11-14 08:33:54 +11:00
{
2023-02-13 07:55:39 -05:00
// Leave IsHit set to true, because the only time it's set to false
// is when a melee weapon is examined. Misses are inferred from an
// empty HitEntities.
// TODO: This needs fixing
2023-08-04 21:17:13 -05:00
if ( meleeUid = = user )
{
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Low ,
$"{ToPrettyString(user):actor} melee attacked (light) using their hands and missed" ) ;
}
else
{
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Low ,
$"{ToPrettyString(user):actor} melee attacked (light) using {ToPrettyString(meleeUid):tool} and missed" ) ;
}
2023-04-27 13:56:51 -05:00
var missEvent = new MeleeHitEvent ( new List < EntityUid > ( ) , user , meleeUid , damage ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( meleeUid , missEvent ) ;
Audio . PlayPredicted ( component . SwingSound , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
return ;
}
// 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
2023-09-11 09:42:41 +10:00
var hitEvent = new MeleeHitEvent ( new List < EntityUid > { target . Value } , user , meleeUid , damage ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( meleeUid , hitEvent ) ;
2022-11-14 08:33:54 +11:00
if ( hitEvent . Handled )
return ;
var targets = new List < EntityUid > ( 1 )
{
2023-09-11 09:42:41 +10:00
target . Value
2022-11-14 08:33:54 +11:00
} ;
2023-09-11 09:42:41 +10:00
var weapon = GetEntity ( ev . Weapon ) ;
Interaction . DoContactInteraction ( weapon , target ) ;
Interaction . DoContactInteraction ( user , weapon ) ;
2022-11-14 08:33:54 +11:00
// If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a
// somewhat messy scuffle. See also, heavy attacks.
2023-09-11 09:42:41 +10:00
Interaction . DoContactInteraction ( user , target ) ;
2022-11-14 08:33:54 +11:00
// For stuff that cares about it being attacked.
2023-05-14 13:15:18 +10:00
var attackedEvent = new AttackedEvent ( meleeUid , user , targetXform . Coordinates ) ;
2023-09-11 09:42:41 +10:00
RaiseLocalEvent ( target . Value , attackedEvent ) ;
2022-11-14 08:33:54 +11:00
2023-05-14 13:15:18 +10:00
var modifiedDamage = DamageSpecifier . ApplyModifierSets ( damage + hitEvent . BonusDamage + attackedEvent . BonusDamage , hitEvent . ModifiersList ) ;
2023-09-11 09:42:41 +10:00
var damageResult = Damageable . TryChangeDamage ( target , modifiedDamage , origin : user ) ;
2022-11-14 08:33:54 +11:00
2023-10-09 03:27:41 +11:00
if ( damageResult ! = null & & damageResult . Any ( ) )
2022-11-14 08:33:54 +11:00
{
// 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 ) )
{
2023-09-11 09:42:41 +10:00
_stamina . TakeStaminaDamage ( target . Value , ( bluntDamage * component . BluntStaminaDamageFactor ) . Float ( ) , visual : false , source : user , with : meleeUid = = user ? null : meleeUid ) ;
2022-11-14 08:33:54 +11:00
}
2023-02-13 07:55:39 -05:00
if ( meleeUid = = user )
2022-11-14 08:33:54 +11:00
{
2023-08-04 21:17:13 -05:00
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Medium ,
2023-09-11 09:42:41 +10:00
$"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using their hands and dealt {damageResult.Total:damage} damage" ) ;
2022-11-14 08:33:54 +11:00
}
else
{
2023-08-04 21:17:13 -05:00
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Medium ,
2023-09-11 09:42:41 +10:00
$"{ToPrettyString(user):actor} melee attacked (light) {ToPrettyString(target.Value):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.Total:damage} damage" ) ;
2022-11-14 08:33:54 +11:00
}
2023-09-11 09:42:41 +10:00
PlayHitSound ( target . Value , user , GetHighestDamageSound ( modifiedDamage , _protoManager ) , hitEvent . HitSoundOverride , component . HitSound ) ;
2022-11-14 08:33:54 +11:00
}
else
{
if ( hitEvent . HitSoundOverride ! = null )
{
2023-02-13 07:55:39 -05:00
Audio . PlayPredicted ( hitEvent . HitSoundOverride , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
}
2023-10-09 03:27:41 +11:00
else if ( ! GetDamage ( meleeUid , user , component ) . Any ( ) & & component . HitSound ! = null )
2023-03-09 19:28:57 +11:00
{
Audio . PlayPredicted ( component . HitSound , meleeUid , user ) ;
}
2022-11-14 08:33:54 +11:00
else
{
2023-02-13 07:55:39 -05:00
Audio . PlayPredicted ( component . NoDamageSound , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
}
}
2022-09-29 15:51:59 +10:00
2022-11-14 08:33:54 +11:00
if ( damageResult ? . Total > FixedPoint2 . Zero )
{
DoDamageEffect ( targets , user , targetXform ) ;
}
2022-09-29 15:51:59 +10:00
}
2022-11-14 08:33:54 +11:00
protected abstract void DoDamageEffect ( List < EntityUid > targets , EntityUid ? user , TransformComponent targetXform ) ;
2023-08-06 12:55:38 +10:00
private bool DoHeavyAttack ( EntityUid user , HeavyAttackEvent ev , EntityUid meleeUid , MeleeWeaponComponent component , ICommonSession ? session )
2022-09-29 15:51:59 +10:00
{
2022-11-14 08:33:54 +11:00
// TODO: This is copy-paste as fuck with DoPreciseAttack
if ( ! TryComp < TransformComponent > ( user , out var userXform ) )
2023-08-06 12:55:38 +10:00
return false ;
2022-11-14 08:33:54 +11:00
2023-09-11 09:42:41 +10:00
var targetMap = GetCoordinates ( ev . Coordinates ) . ToMap ( EntityManager , TransformSystem ) ;
2022-11-14 08:33:54 +11:00
if ( targetMap . MapId ! = userXform . MapID )
2023-08-06 12:55:38 +10:00
return false ;
2023-05-08 17:46:26 +10:00
var userPos = TransformSystem . GetWorldPosition ( userXform ) ;
2022-11-14 08:33:54 +11:00
var direction = targetMap . Position - userPos ;
2023-07-08 14:08:32 +10:00
var distance = Math . Min ( component . Range , direction . Length ( ) ) ;
2022-11-14 08:33:54 +11:00
2023-08-06 16:01:44 +00:00
var damage = GetDamage ( meleeUid , user , component ) ;
2023-09-11 09:42:41 +10:00
var entities = GetEntityList ( ev . Entities ) ;
2022-11-14 08:33:54 +11:00
if ( entities . Count = = 0 )
{
2023-08-04 21:17:13 -05:00
if ( meleeUid = = user )
{
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Low ,
$"{ToPrettyString(user):actor} melee attacked (heavy) using their hands and missed" ) ;
}
else
{
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Low ,
$"{ToPrettyString(user):actor} melee attacked (heavy) using {ToPrettyString(meleeUid):tool} and missed" ) ;
}
2023-04-27 13:56:51 -05:00
var missEvent = new MeleeHitEvent ( new List < EntityUid > ( ) , user , meleeUid , damage ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( meleeUid , missEvent ) ;
Audio . PlayPredicted ( component . SwingSound , meleeUid , user ) ;
2023-08-06 12:55:38 +10:00
return true ;
2022-11-14 08:33:54 +11:00
}
2023-05-08 17:46:26 +10:00
// Naughty input
if ( entities . Count > MaxTargets )
{
entities . RemoveRange ( MaxTargets , entities . Count - MaxTargets ) ;
}
2023-05-02 05:07:17 +10:00
// Validate client
for ( var i = entities . Count - 1 ; i > = 0 ; i - - )
{
if ( ArcRaySuccessful ( entities [ i ] , userPos , direction . ToWorldAngle ( ) , component . Angle , distance ,
userXform . MapID , user , session ) )
{
continue ;
}
// Bad input
entities . RemoveAt ( i ) ;
}
2022-11-14 08:33:54 +11:00
var targets = new List < EntityUid > ( ) ;
var damageQuery = GetEntityQuery < DamageableComponent > ( ) ;
foreach ( var entity in entities )
{
if ( entity = = user | |
! damageQuery . HasComponent ( entity ) )
continue ;
targets . Add ( entity ) ;
}
// 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
2023-04-27 13:56:51 -05:00
var hitEvent = new MeleeHitEvent ( targets , user , meleeUid , damage ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( meleeUid , hitEvent ) ;
2022-11-14 08:33:54 +11:00
if ( hitEvent . Handled )
2023-08-06 12:55:38 +10:00
return true ;
2022-11-14 08:33:54 +11:00
2023-09-11 09:42:41 +10:00
var weapon = GetEntity ( ev . Weapon ) ;
Interaction . DoContactInteraction ( user , weapon ) ;
2022-11-14 08:33:54 +11:00
// For stuff that cares about it being attacked.
foreach ( var target in targets )
{
2023-09-11 09:42:41 +10:00
Interaction . DoContactInteraction ( weapon , target ) ;
2022-11-14 08:33:54 +11:00
// If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a
// somewhat messy scuffle. See also, light attacks.
Interaction . DoContactInteraction ( user , target ) ;
}
var appliedDamage = new DamageSpecifier ( ) ;
foreach ( var entity in targets )
{
2023-09-11 09:42:41 +10:00
var attackedEvent = new AttackedEvent ( meleeUid , user , GetCoordinates ( ev . Coordinates ) ) ;
2023-05-14 13:15:18 +10:00
RaiseLocalEvent ( entity , attackedEvent ) ;
var modifiedDamage = DamageSpecifier . ApplyModifierSets ( damage + hitEvent . BonusDamage + attackedEvent . BonusDamage , hitEvent . ModifiersList ) ;
2022-11-14 08:33:54 +11:00
var damageResult = Damageable . TryChangeDamage ( entity , modifiedDamage , origin : user ) ;
if ( damageResult ! = null & & damageResult . Total > FixedPoint2 . Zero )
{
appliedDamage + = damageResult ;
2023-02-13 07:55:39 -05:00
if ( meleeUid = = user )
2022-11-14 08:33:54 +11:00
{
2023-08-04 21:17:13 -05:00
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Medium ,
$"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.Total:damage} damage" ) ;
2022-11-14 08:33:54 +11:00
}
else
{
2023-08-04 21:17:13 -05:00
AdminLogger . Add ( LogType . MeleeHit , LogImpact . Medium ,
$"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using {ToPrettyString(meleeUid):tool} and dealt {damageResult.Total:damage} damage" ) ;
2022-11-14 08:33:54 +11:00
}
}
}
if ( entities . Count ! = 0 )
{
if ( appliedDamage . Total > FixedPoint2 . Zero )
{
var target = entities . First ( ) ;
2023-05-14 13:15:18 +10:00
PlayHitSound ( target , user , GetHighestDamageSound ( appliedDamage , _protoManager ) , hitEvent . HitSoundOverride , component . HitSound ) ;
2022-11-14 08:33:54 +11:00
}
else
{
if ( hitEvent . HitSoundOverride ! = null )
{
2023-02-13 07:55:39 -05:00
Audio . PlayPredicted ( hitEvent . HitSoundOverride , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
}
else
{
2023-02-13 07:55:39 -05:00
Audio . PlayPredicted ( component . NoDamageSound , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
}
}
}
if ( appliedDamage . Total > FixedPoint2 . Zero )
{
2022-11-15 11:56:10 +11:00
DoDamageEffect ( targets , user , Transform ( targets [ 0 ] ) ) ;
2022-11-14 08:33:54 +11:00
}
2023-08-06 12:55:38 +10:00
return true ;
2022-11-14 08:33:54 +11:00
}
2023-05-02 05:07:17 +10:00
protected HashSet < EntityUid > ArcRayCast ( Vector2 position , Angle angle , Angle arcWidth , float range , MapId mapId , EntityUid ignore )
2022-11-14 08:33:54 +11:00
{
// 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 ;
}
2023-05-02 05:07:17 +10:00
protected virtual bool ArcRaySuccessful ( EntityUid targetUid , Vector2 position , Angle angle , Angle arcWidth , float range ,
MapId mapId , EntityUid ignore , ICommonSession ? session )
{
// Only matters for server.
return true ;
}
2022-11-14 08:33:54 +11:00
private void PlayHitSound ( EntityUid target , EntityUid ? user , 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 . PlayPredicted ( damageSoundComp . NoDamageSound , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
playedSound = true ;
}
else if ( type ! = null & & damageSoundComp . SoundTypes ? . TryGetValue ( type , out var damageSoundType ) = = true )
{
Audio . PlayPredicted ( damageSoundType , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
playedSound = true ;
}
else if ( type ! = null & & damageSoundComp . SoundGroups ? . TryGetValue ( type , out var damageSoundGroup ) = = true )
{
Audio . PlayPredicted ( damageSoundGroup , target , user , 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 . PlayPredicted ( hitSoundOverride , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
playedSound = true ;
}
else if ( hitSound ! = null )
{
Audio . PlayPredicted ( hitSound , target , user , 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" :
2023-06-23 07:23:08 -05:00
case "Radiation" :
2022-11-14 08:33:54 +11:00
case "Cold" :
2022-11-15 11:56:10 +11:00
Audio . PlayPredicted ( new SoundPathSpecifier ( "/Audio/Items/welder.ogg" ) , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
2022-11-14 08:33:54 +11:00
break ;
// No damage, fallback to tappies
case null :
2022-11-15 11:56:10 +11:00
Audio . PlayPredicted ( new SoundPathSpecifier ( "/Audio/Weapons/tap.ogg" ) , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
2022-11-14 08:33:54 +11:00
break ;
case "Brute" :
2022-11-15 11:56:10 +11:00
Audio . PlayPredicted ( new SoundPathSpecifier ( "/Audio/Weapons/smash.ogg" ) , target , user , AudioParams . Default . WithVariation ( DamagePitchVariation ) ) ;
2022-11-14 08:33:54 +11:00
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 ;
2022-09-29 15:51:59 +10:00
}
2023-02-13 07:55:39 -05:00
protected virtual bool DoDisarm ( EntityUid user , DisarmAttackEvent ev , EntityUid meleeUid , MeleeWeaponComponent component , ICommonSession ? session )
2022-09-29 15:51:59 +10:00
{
2023-09-11 09:42:41 +10:00
var target = GetEntity ( ev . Target ) ;
if ( Deleted ( target ) | |
user = = target )
{
2022-09-29 15:51:59 +10:00
return false ;
2023-09-11 09:42:41 +10:00
}
2022-09-29 15:51:59 +10:00
2022-11-14 08:33:54 +11:00
// Play a sound to give instant feedback; same with playing the animations
2023-02-13 07:55:39 -05:00
Audio . PlayPredicted ( component . SwingSound , meleeUid , user ) ;
2022-09-29 15:51:59 +10:00
return true ;
}
2023-10-17 20:12:00 -05:00
private void DoLungeAnimation ( EntityUid user , EntityUid weapon , Angle angle , MapCoordinates coordinates , float length , string? animation )
2022-09-29 15:51:59 +10:00
{
// TODO: Assert that offset eyes are still okay.
if ( ! TryComp < TransformComponent > ( user , out var userXform ) )
return ;
2023-05-08 17:46:26 +10:00
var invMatrix = TransformSystem . GetInvWorldMatrix ( userXform ) ;
2022-09-29 15:51:59 +10:00
var localPos = invMatrix . Transform ( coordinates . Position ) ;
2023-07-08 14:08:32 +10:00
if ( localPos . LengthSquared ( ) < = 0f )
2022-09-29 15:51:59 +10:00
return ;
localPos = userXform . LocalRotation . RotateVec ( localPos ) ;
2022-11-09 07:28:49 +11:00
// We'll play the effect just short visually so it doesn't look like we should be hitting but actually aren't.
2023-03-12 15:56:05 +11:00
const float bufferLength = 0.2f ;
var visualLength = length - bufferLength ;
2022-11-09 07:28:49 +11:00
2023-07-08 14:08:32 +10:00
if ( localPos . Length ( ) > visualLength )
localPos = localPos . Normalized ( ) * visualLength ;
2022-11-09 07:28:49 +11:00
2023-10-17 20:12:00 -05:00
DoLunge ( user , weapon , angle , localPos , animation ) ;
2022-09-29 15:51:59 +10:00
}
2023-10-17 20:12:00 -05:00
public abstract void DoLunge ( EntityUid user , EntityUid weapon , Angle angle , Vector2 localPos , string? animation , bool predicted = true ) ;
2022-09-29 15:51:59 +10:00
}