2022-11-14 08:33:54 +11:00
using System.Linq ;
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 ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Audio ;
2022-09-29 15:51:59 +10:00
using Robust.Shared.GameStates ;
using Robust.Shared.Map ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Physics ;
using Robust.Shared.Physics.Systems ;
2022-10-17 15:54:31 +11:00
using Robust.Shared.Players ;
2022-11-14 08:33:54 +11:00
using Robust.Shared.Prototypes ;
2022-09-29 15:51:59 +10:00
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 ! ;
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 ! ;
[Dependency] protected 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-02-13 07:55:39 -05:00
[Dependency] private readonly SharedTransformSystem _transform = default ! ;
2022-11-14 08:33:54 +11:00
[Dependency] private readonly StaminaSystem _stamina = default ! ;
2022-09-29 15:51:59 +10:00
protected ISawmill Sawmill = default ! ;
2022-11-14 08:33:54 +11:00
public const float DamagePitchVariation = 0.05f ;
private const int AttackMask = ( int ) ( CollisionGroup . MobMask | CollisionGroup . Opaque ) ;
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 ( ) ;
Sawmill = Logger . GetSawmill ( "melee" ) ;
SubscribeLocalEvent < MeleeWeaponComponent , ComponentGetState > ( OnGetState ) ;
SubscribeLocalEvent < MeleeWeaponComponent , ComponentHandleState > ( OnHandleState ) ;
2022-10-04 12:12:45 +11:00
SubscribeLocalEvent < MeleeWeaponComponent , HandDeselectedEvent > ( OnMeleeDropped ) ;
2022-11-04 12:18:00 +11:00
SubscribeLocalEvent < MeleeWeaponComponent , HandSelectedEvent > ( OnMeleeSelected ) ;
2022-09-29 15:51:59 +10:00
SubscribeAllEvent < LightAttackEvent > ( OnLightAttack ) ;
SubscribeAllEvent < StartHeavyAttackEvent > ( OnStartHeavyAttack ) ;
SubscribeAllEvent < StopHeavyAttackEvent > ( OnStopHeavyAttack ) ;
SubscribeAllEvent < HeavyAttackEvent > ( OnHeavyAttack ) ;
SubscribeAllEvent < DisarmAttackEvent > ( OnDisarmAttack ) ;
SubscribeAllEvent < StopAttackEvent > ( OnStopAttack ) ;
}
2022-11-04 12:18:00 +11:00
private void OnMeleeSelected ( EntityUid uid , MeleeWeaponComponent component , HandSelectedEvent args )
{
if ( component . AttackRate . Equals ( 0f ) )
return ;
2023-01-27 03:04:26 +02:00
if ( ! component . ResetOnHandSelected )
return ;
2022-11-04 12:18:00 +11:00
// If someone swaps to this weapon then reset its cd.
var curTime = Timing . CurTime ;
var minimum = curTime + TimeSpan . FromSeconds ( 1 / component . AttackRate ) ;
if ( minimum < component . NextAttack )
return ;
component . NextAttack = minimum ;
Dirty ( component ) ;
}
2022-10-04 12:12:45 +11:00
private void OnMeleeDropped ( EntityUid uid , MeleeWeaponComponent component , HandDeselectedEvent args )
{
if ( component . WindUpStart = = null )
return ;
component . WindUpStart = null ;
Dirty ( component ) ;
}
2022-09-29 15:51:59 +10:00
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 ;
2023-02-13 07:55:39 -05:00
AttemptAttack ( args . SenderSession . AttachedEntity ! . Value , msg . Weapon , weapon , msg , args . SenderSession ) ;
2022-09-29 15:51:59 +10:00
}
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 ;
2023-02-13 07:55:39 -05:00
AttemptAttack ( args . SenderSession . AttachedEntity . Value , msg . Weapon , 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 ;
}
var userWeapon = GetWeapon ( args . SenderSession . AttachedEntity . Value ) ;
if ( userWeapon = = null )
return ;
2023-02-13 07:55:39 -05:00
AttemptAttack ( args . SenderSession . AttachedEntity . Value , userWeapon . Owner , userWeapon , msg , args . SenderSession ) ;
2022-09-29 15:51:59 +10:00
}
private void OnGetState ( EntityUid uid , MeleeWeaponComponent component , ref ComponentGetState args )
{
args . State = new MeleeWeaponComponentState ( component . AttackRate , component . Attacking , component . NextAttack ,
2022-11-19 11:07:09 -05:00
component . WindUpStart , component . ClickAnimation , component . WideAnimation , component . Range ) ;
2022-09-29 15:51:59 +10:00
}
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 ;
2022-11-19 11:07:09 -05:00
component . ClickAnimation = state . ClickAnimation ;
component . WideAnimation = state . WideAnimation ;
component . Range = state . Range ;
2022-09-29 15:51:59 +10:00
}
public MeleeWeaponComponent ? GetWeapon ( EntityUid entity )
{
MeleeWeaponComponent ? melee ;
2022-12-10 12:05:39 -05:00
var ev = new GetMeleeWeaponEvent ( ) ;
RaiseLocalEvent ( entity , ev ) ;
if ( ev . Handled )
{
return EntityManager . GetComponentOrNull < MeleeWeaponComponent > ( ev . Weapon ) ;
}
2022-09-29 15:51:59 +10:00
// 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 ;
}
2022-10-16 07:20:05 +11:00
// Use hands clothing if applicable.
if ( Inventory . TryGetSlotEntity ( entity , "gloves" , out var gloves ) & &
TryComp < MeleeWeaponComponent > ( gloves , out var glovesMelee ) )
{
return glovesMelee ;
}
// Use our own melee
2022-09-29 15:51:59 +10:00
if ( TryComp ( entity , out melee ) )
{
return melee ;
}
return null ;
}
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-02-13 07:55:39 -05:00
AttemptAttack ( user , weaponUid , weapon , new LightAttackEvent ( null , weaponUid , coordinates ) , null ) ;
2022-12-12 00:37:09 +11:00
}
2022-09-29 15:51:59 +10:00
2023-02-13 07:55:39 -05:00
public void AttemptLightAttack ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , EntityUid target )
2022-09-29 15:51:59 +10:00
{
if ( ! TryComp < TransformComponent > ( target , out var targetXform ) )
return ;
2023-02-13 07:55:39 -05:00
AttemptAttack ( user , weaponUid , weapon , new LightAttackEvent ( target , weaponUid , targetXform . Coordinates ) , null ) ;
2022-09-29 15:51:59 +10:00
}
2023-02-13 07:55:39 -05:00
public void AttemptDisarmAttack ( EntityUid user , EntityUid weaponUid , MeleeWeaponComponent weapon , EntityUid target )
2022-09-29 15:51:59 +10:00
{
if ( ! TryComp < TransformComponent > ( target , out var targetXform ) )
return ;
2023-02-13 07:55:39 -05:00
AttemptAttack ( user , weaponUid , weapon , new DisarmAttackEvent ( target , 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-02-13 07:55:39 -05:00
private void 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 )
return ;
2022-10-04 12:50:09 +11:00
if ( ! CombatMode . IsInCombatMode ( user ) )
return ;
2022-12-17 14:47:15 +11:00
switch ( attack )
{
case LightAttackEvent light :
if ( ! Blocker . CanAttack ( user , light . Target ) )
return ;
break ;
case DisarmAttackEvent disarm :
if ( ! Blocker . CanAttack ( user , disarm . Target ) )
return ;
break ;
default :
if ( ! Blocker . CanAttack ( user ) )
return ;
break ;
}
2022-09-29 15:51:59 +10:00
// Windup time checked elsewhere.
if ( weapon . NextAttack < curTime )
weapon . NextAttack = curTime ;
weapon . NextAttack + = TimeSpan . FromSeconds ( 1f / weapon . AttackRate ) ;
// Attack confirmed
2022-11-09 07:28:49 +11:00
string animation ;
2022-09-29 15:51:59 +10:00
switch ( attack )
{
case LightAttackEvent light :
2023-02-13 07:55:39 -05:00
DoLightAttack ( user , light , weaponUid , weapon , session ) ;
2022-11-09 07:28:49 +11:00
animation = weapon . ClickAnimation ;
2022-09-29 15:51:59 +10:00
break ;
case DisarmAttackEvent disarm :
2023-02-13 07:55:39 -05:00
if ( ! DoDisarm ( user , disarm , weaponUid , weapon , session ) )
2022-10-15 15:14:07 +11:00
return ;
2022-11-09 07:28:49 +11:00
animation = weapon . ClickAnimation ;
2022-09-29 15:51:59 +10:00
break ;
case HeavyAttackEvent heavy :
2023-02-13 07:55:39 -05:00
DoHeavyAttack ( user , heavy , weaponUid , weapon , session ) ;
2022-11-09 07:28:49 +11:00
animation = weapon . WideAnimation ;
2022-09-29 15:51:59 +10:00
break ;
default :
throw new NotImplementedException ( ) ;
}
2022-11-09 07:28:49 +11:00
DoLungeAnimation ( user , weapon . Angle , attack . Coordinates . ToMap ( EntityManager ) , weapon . Range , animation ) ;
2022-09-29 15:51:59 +10:00
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 ( ) ;
}
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-02-13 07:55:39 -05:00
var damage = component . Damage * GetModifier ( component , true ) ;
2022-11-14 08:33:54 +11:00
// Can't attack yourself
2023-02-13 07:55:39 -05:00
// For consistency with wide attacks stuff needs damageable.
2022-11-14 08:33:54 +11:00
if ( user = = ev . Target | |
Deleted ( ev . Target ) | |
! HasComp < DamageableComponent > ( ev . Target ) | |
2023-02-13 07:55:39 -05:00
! TryComp < TransformComponent > ( ev . Target , out var targetXform ) | |
// Not in LOS.
! InRange ( user , ev . 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
var missEvent = new MeleeHitEvent ( new List < EntityUid > ( ) , user , damage ) ;
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
var hitEvent = new MeleeHitEvent ( new List < EntityUid > { ev . Target . Value } , user , 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 )
{
ev . Target . Value
} ;
Interaction . DoContactInteraction ( ev . Weapon , ev . Target ) ;
Interaction . DoContactInteraction ( user , ev . Weapon ) ;
// 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.
Interaction . DoContactInteraction ( user , ev . Target ) ;
// For stuff that cares about it being attacked.
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( ev . Target . Value , new AttackedEvent ( meleeUid , user , targetXform . Coordinates ) ) ;
2022-11-14 08:33:54 +11:00
var modifiedDamage = DamageSpecifier . ApplyModifierSets ( damage + hitEvent . BonusDamage , hitEvent . ModifiersList ) ;
var damageResult = Damageable . TryChangeDamage ( ev . Target , modifiedDamage , origin : user ) ;
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 ) )
{
2022-12-11 22:21:15 -06:00
_stamina . TakeStaminaDamage ( ev . Target . Value , ( bluntDamage * component . BluntStaminaDamageFactor ) . Float ( ) , source : user , with : ( component . Owner = = user ? null : component . Owner ) ) ;
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
{
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 , user , GetHighestDamageSound ( modifiedDamage , _protoManager ) , hitEvent . HitSoundOverride , component . HitSound ) ;
}
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-03-09 19:28:57 +11:00
else if ( component . Damage . Total . Equals ( FixedPoint2 . Zero ) & & component . HitSound ! = null )
{
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-02-13 07:55:39 -05:00
protected virtual void 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 ) )
{
return ;
}
var targetMap = ev . Coordinates . ToMap ( EntityManager ) ;
if ( targetMap . MapId ! = userXform . MapID )
{
return ;
}
2022-09-29 15:51:59 +10:00
2023-02-13 07:55:39 -05:00
var userPos = _transform . GetWorldPosition ( userXform ) ;
2022-11-14 08:33:54 +11:00
var direction = targetMap . Position - userPos ;
var distance = Math . Min ( component . Range , direction . Length ) ;
2023-02-13 07:55:39 -05:00
var damage = component . Damage * GetModifier ( component , false ) ;
2022-11-14 08:33:54 +11:00
// 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 )
{
2023-02-13 07:55:39 -05:00
var missEvent = new MeleeHitEvent ( new List < EntityUid > ( ) , user , damage ) ;
RaiseLocalEvent ( meleeUid , missEvent ) ;
Audio . PlayPredicted ( component . SwingSound , meleeUid , user ) ;
2022-11-14 08:33:54 +11:00
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 ) ;
}
// 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 ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( meleeUid , hitEvent ) ;
2022-11-14 08:33:54 +11:00
if ( hitEvent . Handled )
return ;
Interaction . DoContactInteraction ( user , ev . Weapon ) ;
// For stuff that cares about it being attacked.
foreach ( var target in targets )
{
Interaction . DoContactInteraction ( ev . Weapon , target ) ;
// 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 ) ;
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( target , new AttackedEvent ( meleeUid , user , Transform ( target ) . Coordinates ) ) ;
2022-11-14 08:33:54 +11:00
}
var modifiedDamage = DamageSpecifier . ApplyModifierSets ( damage + hitEvent . BonusDamage , hitEvent . ModifiersList ) ;
var appliedDamage = new DamageSpecifier ( ) ;
foreach ( var entity in targets )
{
2023-02-13 07:55:39 -05:00
RaiseLocalEvent ( entity , new AttackedEvent ( meleeUid , user , ev . Coordinates ) ) ;
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
{
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 , user , GetHighestDamageSound ( modifiedDamage , _protoManager ) , hitEvent . HitSoundOverride , component . HitSound ) ;
}
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
}
}
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 ;
}
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" :
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
{
if ( Deleted ( ev . Target ) | |
user = = ev . Target )
return false ;
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 ;
}
2022-11-09 07:28:49 +11:00
private void DoLungeAnimation ( EntityUid user , 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 ;
var invMatrix = userXform . InvWorldMatrix ;
var localPos = invMatrix . Transform ( coordinates . Position ) ;
if ( localPos . LengthSquared < = 0f )
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.
const float BufferLength = 0.2f ;
var visualLength = length - BufferLength ;
if ( localPos . Length > visualLength )
localPos = localPos . Normalized * visualLength ;
2022-09-29 15:51:59 +10:00
DoLunge ( user , angle , localPos , animation ) ;
}
public abstract void DoLunge ( EntityUid user , Angle angle , Vector2 localPos , string? animation ) ;
}