diff --git a/Content.Server/Execution/ExecutionSystem.cs b/Content.Server/Execution/ExecutionSystem.cs deleted file mode 100644 index 4354608ca3..0000000000 --- a/Content.Server/Execution/ExecutionSystem.cs +++ /dev/null @@ -1,397 +0,0 @@ -using Content.Server.Interaction; -using Content.Server.Kitchen.Components; -using Content.Server.Weapons.Ranged.Systems; -using Content.Shared.ActionBlocker; -using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.Execution; -using Content.Shared.Interaction.Components; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Popups; -using Content.Shared.Projectiles; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee; -using Content.Shared.Weapons.Ranged; -using Content.Shared.Weapons.Ranged.Components; -using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.Weapons.Ranged.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; - -namespace Content.Server.Execution; - -/// -/// Verb for violently murdering cuffed creatures. -/// -public sealed class ExecutionSystem : EntitySystem -{ - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly InteractionSystem _interactionSystem = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IComponentFactory _componentFactory = default!; - [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly GunSystem _gunSystem = default!; - - private const float MeleeExecutionTimeModifier = 5.0f; - private const float GunExecutionTime = 6.0f; - private const float DamageModifier = 9.0f; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnGetInteractionVerbsMelee); - SubscribeLocalEvent>(OnGetInteractionVerbsGun); - - SubscribeLocalEvent(OnDoafterMelee); - SubscribeLocalEvent(OnDoafterGun); - } - - private void OnGetInteractionVerbsMelee( - EntityUid uid, - SharpComponent component, - GetVerbsEvent args) - { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) - return; - - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - - if (!CanExecuteWithMelee(weapon, victim, attacker)) - return; - - UtilityVerb verb = new() - { - Act = () => - { - TryStartMeleeExecutionDoafter(weapon, victim, attacker); - }, - Impact = LogImpact.High, - Text = Loc.GetString("execution-verb-name"), - Message = Loc.GetString("execution-verb-message"), - }; - - args.Verbs.Add(verb); - } - - private void OnGetInteractionVerbsGun( - EntityUid uid, - GunComponent component, - GetVerbsEvent args) - { - if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) - return; - - var attacker = args.User; - var weapon = args.Using!.Value; - var victim = args.Target; - - if (!CanExecuteWithGun(weapon, victim, attacker)) - return; - - UtilityVerb verb = new() - { - Act = () => - { - TryStartGunExecutionDoafter(weapon, victim, attacker); - }, - Impact = LogImpact.High, - Text = Loc.GetString("execution-verb-name"), - Message = Loc.GetString("execution-verb-message"), - }; - - args.Verbs.Add(verb); - } - - private bool CanExecuteWithAny(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - // No point executing someone if they can't take damage - if (!TryComp(victim, out var damage)) - return false; - - // You can't execute something that cannot die - if (!TryComp(victim, out var mobState)) - return false; - - // You're not allowed to execute dead people (no fun allowed) - if (_mobStateSystem.IsDead(victim, mobState)) - return false; - - // You must be able to attack people to execute - if (!_actionBlockerSystem.CanAttack(attacker, victim)) - return false; - - // The victim must be incapacitated to be executed - if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null)) - return false; - - // All checks passed - return true; - } - - private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually hurt people with the weapon - if (!TryComp(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) - return false; - - return true; - } - - private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user) - { - if (!CanExecuteWithAny(weapon, victim, user)) return false; - - // We must be able to actually fire the gun - if (!TryComp(weapon, out var gun) && _gunSystem.CanShoot(gun!)) - return false; - - return true; - } - - private void TryStartMeleeExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - if (!CanExecuteWithMelee(weapon, victim, attacker)) - return; - - var executionTime = (1.0f / Comp(weapon).AttackRate) * MeleeExecutionTimeModifier; - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - - var doAfter = - new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnDamage = true, - NeedHand = true - }; - - _doAfterSystem.TryStartDoAfter(doAfter); - } - - private void TryStartGunExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker) - { - if (!CanExecuteWithGun(weapon, victim, attacker)) - return; - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - - var doAfter = - new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - BreakOnDamage = true, - NeedHand = true - }; - - _doAfterSystem.TryStartDoAfter(doAfter); - } - - private bool OnDoafterChecks(EntityUid uid, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return false; - - if (!CanExecuteWithAny(args.Used.Value, args.Target.Value, uid)) - return false; - - // All checks passed - return true; - } - - private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return; - - var attacker = args.User; - var victim = args.Target!.Value; - var weapon = args.Used!.Value; - - if (!CanExecuteWithMelee(weapon, victim, attacker)) return; - - if (!TryComp(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) - return; - - _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true); - _audioSystem.PlayEntity(melee.HitSound, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - - if (attacker == victim) - { - ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - } - } - - // TODO: This repeats a lot of the code of the serverside GunSystem, make it not do that - private void OnDoafterGun(EntityUid uid, GunComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) - return; - - var attacker = args.User; - var weapon = args.Used!.Value; - var victim = args.Target!.Value; - - if (!CanExecuteWithGun(weapon, victim, attacker)) return; - - // Check if any systems want to block our shot - var prevention = new ShotAttemptedEvent - { - User = attacker, - Used = weapon - }; - - RaiseLocalEvent(weapon, ref prevention); - if (prevention.Cancelled) - return; - - RaiseLocalEvent(attacker, ref prevention); - if (prevention.Cancelled) - return; - - // Not sure what this is for but gunsystem uses it so ehhh - var attemptEv = new AttemptShootEvent(attacker, null); - RaiseLocalEvent(weapon, ref attemptEv); - - if (attemptEv.Cancelled) - { - if (attemptEv.Message != null) - { - _popupSystem.PopupClient(attemptEv.Message, weapon, attacker); - return; - } - } - - // Take some ammunition for the shot (one bullet) - var fromCoordinates = Transform(attacker).Coordinates; - var ev = new TakeAmmoEvent(1, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, attacker); - RaiseLocalEvent(weapon, ev); - - // Check if there's any ammo left - if (ev.Ammo.Count <= 0) - { - _audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon); - return; - } - - // Information about the ammo like damage - DamageSpecifier damage = new DamageSpecifier(); - - // Get some information from IShootable - var ammoUid = ev.Ammo[0].Entity; - switch (ev.Ammo[0].Shootable) - { - case CartridgeAmmoComponent cartridge: - // Get the damage value - var prototype = _prototypeManager.Index(cartridge.Prototype); - prototype.TryGetComponent(out var projectileA, _componentFactory); // sloth forgive me - if (projectileA != null) - { - damage = projectileA.Damage * cartridge.Count; - } - - // Expend the cartridge - cartridge.Spent = true; - _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true); - Dirty(ammoUid.Value, cartridge); - - break; - - case AmmoComponent newAmmo: - TryComp(ammoUid, out var projectileB); - if (projectileB != null) - { - damage = projectileB.Damage; - } - Del(ammoUid); - break; - - case HitscanPrototype hitscan: - damage = hitscan.Damage!; - break; - - default: - throw new ArgumentOutOfRangeException(); - } - - // Clumsy people have a chance to shoot themselves - if (TryComp(attacker, out var clumsy) && component.ClumsyProof == false) - { - if (_interactionSystem.TryRollClumsy(attacker, 0.33333333f, clumsy)) - { - ShowExecutionPopup("execution-popup-gun-clumsy-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-clumsy-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); - - // You shoot yourself with the gun (no damage multiplier) - _damageableSystem.TryChangeDamage(attacker, damage, origin: attacker); - _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, true, AudioParams.Default); - return; - } - } - - // Gun successfully fired, deal damage - _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true); - _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default); - - // Popups - if (attacker != victim) - { - ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); - ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } - else - { - ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon); - ShowExecutionPopup("suicide-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); - } - } - - private void ShowExecutionPopup(string locString, Filter filter, PopupType type, - EntityUid attacker, EntityUid victim, EntityUid weapon) - { - _popupSystem.PopupEntity(Loc.GetString( - locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), - attacker, filter, true, type); - } -} \ No newline at end of file diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs index 15ea5936e1..80c5f039d3 100644 --- a/Content.Server/Projectiles/ProjectileSystem.cs +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -28,10 +28,14 @@ public sealed class ProjectileSystem : SharedProjectileSystem { // This is so entities that shouldn't get a collision are ignored. if (args.OurFixtureId != ProjectileFixture || !args.OtherFixture.Hard - || component.DamagedEntity || component is { Weapon: null, OnlyCollideWhenShot: true }) + || component.DamagedEntity || component is + { Weapon: null, OnlyCollideWhenShot: true }) + { return; + } var target = args.OtherEntity; + // it's here so this check is only done once before possible hit var attemptEv = new ProjectileReflectAttemptEvent(uid, component, false); RaiseLocalEvent(target, ref attemptEv); @@ -41,11 +45,26 @@ public sealed class ProjectileSystem : SharedProjectileSystem return; } + if (TryHandleProjectile(target, (uid, component))) + { + var direction = args.OurBody.LinearVelocity.Normalized(); + _sharedCameraRecoil.KickCamera(target, direction); + } + } + + /// + /// Tries to handle a projectile interacting with the target. + /// + /// True if the target isn't deleted. + public bool TryHandleProjectile(EntityUid target, Entity projectile) + { + var uid = projectile.Owner; + var component = projectile.Comp; + var ev = new ProjectileHitEvent(component.Damage, target, component.Shooter); RaiseLocalEvent(uid, ref ev); var otherName = ToPrettyString(target); - var direction = args.OurBody.LinearVelocity.Normalized(); var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, origin: component.Shooter); var deleted = Deleted(target); @@ -64,12 +83,11 @@ public sealed class ProjectileSystem : SharedProjectileSystem if (!deleted) { _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound); - _sharedCameraRecoil.KickCamera(target, direction); } component.DamagedEntity = true; - var afterProjectileHitEvent = new AfterProjectileHitEvent(component.Damage, target, args.OtherFixture); + var afterProjectileHitEvent = new AfterProjectileHitEvent(component.Damage, target); RaiseLocalEvent(uid, ref afterProjectileHitEvent); if (component.DeleteOnCollide) @@ -79,5 +97,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem { RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager)); } + + return !deleted; } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index b8f8f12211..fd3b6c5db3 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Administration.Logs; using Content.Server.Cargo.Systems; using Content.Server.Interaction; using Content.Server.Power.EntitySystems; +using Content.Server.Projectiles; using Content.Server.Stunnable; using Content.Server.Weapons.Ranged.Components; using Content.Shared.Damage; @@ -29,13 +30,13 @@ namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem : SharedGunSystem { - [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; + [Dependency] private readonly ProjectileSystem _projectile = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StunSystem _stun = default!; @@ -65,6 +66,137 @@ public sealed partial class GunSystem : SharedGunSystem args.Price += price * component.UnspawnedCount; } + protected override bool ShootDirect(EntityUid gunUid, GunComponent gun, EntityUid target, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityUid user) + { + var result = false; + + // TODO: This is dogshit. I just want to get executions slightly better. + // Ideally you'd pull out cartridge + ammo to separate handling functions and re-use it here, then hitscan you need to bypass entirely. + // You should also make shooting into a struct of args given how many there are now. + var fromCoordinates = Transform(gunUid).Coordinates; + var toCoordinates = Transform(target).Coordinates; + + var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem); + var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); + var mapDirection = toMap - fromMap.Position; + var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); + + // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. + var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out _) + ? fromCoordinates.WithEntityId(gridUid, EntityManager) + : new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position); + + // I must be high because this was getting tripped even when true. + // DebugTools.Assert(direction != Vector2.Zero); + var shotProjectiles = new List(ammo.Count); + var cartridgeBullets = new List(); + + foreach (var (ent, shootable) in ammo) + { + switch (shootable) + { + // Cartridge shoots something else + case CartridgeAmmoComponent cartridge: + if (!cartridge.Spent) + { + for (var i = 0; i < cartridge.Count; i++) + { + var uid = Spawn(cartridge.Prototype, fromEnt); + cartridgeBullets.Add(uid); + } + + RaiseLocalEvent(ent!.Value, new AmmoShotEvent() + { + FiredProjectiles = cartridgeBullets, + }); + + shotProjectiles.AddRange(cartridgeBullets); + cartridgeBullets.Clear(); + SetCartridgeSpent(ent.Value, cartridge, true); + MuzzleFlash(gunUid, cartridge, user); + Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + + if (cartridge.DeleteOnSpawn) + Del(ent.Value); + } + else + { + Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); + } + + // Something like ballistic might want to leave it in the container still + if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value)) + EjectCartridge(ent.Value, angle); + + result = true; + Dirty(ent!.Value, cartridge); + break; + // Ammo shoots itself + case AmmoComponent newAmmo: + result = true; + shotProjectiles.Add(ent!.Value); + MuzzleFlash(gunUid, newAmmo, user); + Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + break; + case HitscanPrototype hitscan: + result = true; + var hitEntity = target; + if (hitscan.StaminaDamage > 0f) + _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user); + + var dmg = hitscan.Damage; + + var hitName = ToPrettyString(hitEntity); + if (dmg != null) + dmg = Damageable.TryChangeDamage(hitEntity, dmg, origin: user); + + // check null again, as TryChangeDamage returns modified damage values + if (dmg != null) + { + if (!Deleted(hitEntity)) + { + if (dmg.Any()) + { + _color.RaiseEffect(Color.Red, new List() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager)); + } + + // TODO get fallback position for playing hit sound. + PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound); + } + + Logs.Add(LogType.HitScanHit, + $"{ToPrettyString(user):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage"); + } + + Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + foreach (var ammoUid in shotProjectiles) + { + // TODO: Handle this shit + if (!TryComp(ammoUid, out ProjectileComponent? projectileComponent)) + { + QueueDel(ammoUid); + continue; + } + + _projectile.TryHandleProjectile(target, (ammoUid, projectileComponent)); + // Even this deletion handling is mega sussy. + Del(ammoUid); + } + + RaiseLocalEvent(gunUid, new AmmoShotEvent() + { + FiredProjectiles = shotProjectiles, + }); + + return result; + } + public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false) { @@ -72,7 +204,7 @@ public sealed partial class GunSystem : SharedGunSystem // Try a clumsy roll // TODO: Who put this here - if (TryComp(user, out var clumsy) && gun.ClumsyProof == false) + if (TryComp(user, out var clumsy) && !gun.ClumsyProof) { for (var i = 0; i < ammo.Count; i++) { @@ -93,6 +225,8 @@ public sealed partial class GunSystem : SharedGunSystem } } + // As the above message wasn't obvious stop putting stuff here and use events + var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem); var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); var mapDirection = toMap - fromMap.Position; @@ -100,7 +234,7 @@ public sealed partial class GunSystem : SharedGunSystem var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. - var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out var grid) + var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out _) ? fromCoordinates.WithEntityId(gridUid, EntityManager) : new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position); @@ -112,6 +246,7 @@ public sealed partial class GunSystem : SharedGunSystem // I must be high because this was getting tripped even when true. // DebugTools.Assert(direction != Vector2.Zero); var shotProjectiles = new List(ammo.Count); + var cartridgeBullets = new List(); foreach (var (ent, shootable) in ammo) { @@ -140,21 +275,23 @@ public sealed partial class GunSystem : SharedGunSystem { var uid = Spawn(cartridge.Prototype, fromEnt); ShootOrThrow(uid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); - shotProjectiles.Add(uid); + cartridgeBullets.Add(uid); } } else { var uid = Spawn(cartridge.Prototype, fromEnt); ShootOrThrow(uid, mapDirection, gunVelocity, gun, gunUid, user); - shotProjectiles.Add(uid); + cartridgeBullets.Add(uid); } RaiseLocalEvent(ent!.Value, new AmmoShotEvent() { - FiredProjectiles = shotProjectiles, + FiredProjectiles = cartridgeBullets, }); + shotProjectiles.AddRange(cartridgeBullets); + cartridgeBullets.Clear(); SetCartridgeSpent(ent.Value, cartridge, true); MuzzleFlash(gunUid, cartridge, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); diff --git a/Content.Shared/Execution/ExecutionComponent.cs b/Content.Shared/Execution/ExecutionComponent.cs new file mode 100644 index 0000000000..f9c5111d63 --- /dev/null +++ b/Content.Shared/Execution/ExecutionComponent.cs @@ -0,0 +1,26 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Execution; + +/// +/// Added to entities that can be used to execute another target. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ExecutionComponent : Component +{ + /// + /// How long the execution duration lasts. + /// + [DataField, AutoNetworkedField] + public float DoAfterDuration = 5f; + + [DataField, AutoNetworkedField] + public float DamageModifier = 9f; + + // Not networked because this is transient inside of a tick. + /// + /// True if it is currently executing for handlers. + /// + [DataField] + public bool Executing = true; +} diff --git a/Content.Shared/Execution/ExecutionSystem.cs b/Content.Shared/Execution/ExecutionSystem.cs new file mode 100644 index 0000000000..de6db205be --- /dev/null +++ b/Content.Shared/Execution/ExecutionSystem.cs @@ -0,0 +1,241 @@ +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.ActionBlocker; +using Content.Shared.CombatMode; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Shared.Execution; + +/// +/// Verb for violently murdering cuffed creatures. +/// +public sealed class ExecutionSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] private readonly SharedGunSystem _gunSystem = default!; + [Dependency] private readonly SharedCombatModeSystem _combatSystem = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _meleeSystem = default!; + + // TODO: Still needs more cleaning up. + private const string DefaultInternalMeleeExecutionMessage = "execution-popup-melee-initial-internal"; + private const string DefaultExternalMeleeExecutionMessage = "execution-popup-melee-initial-external"; + private const string DefaultCompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal"; + private const string DefaultCompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external"; + private const string DefaultInternalGunExecutionMessage = "execution-popup-gun-initial-internal"; + private const string DefaultExternalGunExecutionMessage = "execution-popup-gun-initial-external"; + private const string DefaultCompleteInternalGunExecutionMessage = "execution-popup-gun-complete-internal"; + private const string DefaultCompleteExternalGunExecutionMessage = "execution-popup-gun-complete-external"; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetInteractionsVerbs); + SubscribeLocalEvent(OnExecutionDoAfter); + SubscribeLocalEvent(OnGetMeleeDamage); + } + + private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent args) + { + if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract) + return; + + var attacker = args.User; + var weapon = args.Using.Value; + var victim = args.Target; + + if (!CanExecuteWithAny(victim, attacker)) + return; + + UtilityVerb verb = new() + { + Act = () => TryStartExecutionDoAfter(weapon, victim, attacker, comp), + Impact = LogImpact.High, + Text = Loc.GetString("execution-verb-name"), + Message = Loc.GetString("execution-verb-message"), + }; + + args.Verbs.Add(verb); + } + + private void TryStartExecutionDoAfter(EntityUid weapon, EntityUid victim, EntityUid attacker, ExecutionComponent comp) + { + if (!CanExecuteWithAny(victim, attacker)) + return; + + // TODO: This should just be on the weapons as a single execution message. + var defaultExecutionInternal = DefaultInternalMeleeExecutionMessage; + var defaultExecutionExternal = DefaultExternalMeleeExecutionMessage; + + if (HasComp(weapon)) + { + defaultExecutionExternal = DefaultInternalGunExecutionMessage; + defaultExecutionInternal = DefaultExternalGunExecutionMessage; + } + + var internalMsg = defaultExecutionInternal; + var externalMsg = defaultExecutionExternal; + ShowExecutionInternalPopup(internalMsg, attacker, victim, weapon); + ShowExecutionExternalPopup(externalMsg, attacker, victim, weapon); + + var doAfter = + new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _doAfterSystem.TryStartDoAfter(doAfter); + + } + + private bool CanExecuteWithAny(EntityUid victim, EntityUid attacker) + { + // Use suicide. + if (victim == attacker) + return false; + + // No point executing someone if they can't take damage + if (!TryComp(victim, out _)) + return false; + + // You can't execute something that cannot die + if (!TryComp(victim, out var mobState)) + return false; + + // You're not allowed to execute dead people (no fun allowed) + if (_mobStateSystem.IsDead(victim, mobState)) + return false; + + // You must be able to attack people to execute + if (!_actionBlockerSystem.CanAttack(attacker, victim)) + return false; + + // The victim must be incapacitated to be executed + if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null)) + return false; + + // All checks passed + return true; + } + + private void OnExecutionDoAfter(EntityUid uid, ExecutionComponent component, ExecutionDoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) + return; + + var attacker = args.User; + var victim = args.Target.Value; + var weapon = args.Used.Value; + + if (!CanExecuteWithAny(victim, attacker)) + return; + + // This is needed so the melee system does not stop it. + var prev = _combatSystem.IsInCombatMode(attacker); + _combatSystem.SetInCombatMode(attacker, true); + component.Executing = true; + string? internalMsg = null; + string? externalMsg = null; + + if (TryComp(uid, out MeleeWeaponComponent? melee)) + { + _meleeSystem.AttemptLightAttack(attacker, weapon, melee, victim); + internalMsg = DefaultCompleteInternalMeleeExecutionMessage; + externalMsg = DefaultCompleteExternalMeleeExecutionMessage; + } + else if (TryComp(uid, out GunComponent? gun)) + { + var clumsyShot = false; + + // TODO: This should just be an event or something instead to get this. + // TODO: Handle clumsy. + if (!_gunSystem.AttemptDirectShoot(args.User, uid, args.Target.Value, gun)) + { + internalMsg = null; + externalMsg = null; + } + else + { + internalMsg = DefaultCompleteInternalGunExecutionMessage; + externalMsg = DefaultCompleteExternalGunExecutionMessage; + } + args.Handled = true; + } + + _combatSystem.SetInCombatMode(attacker, prev); + component.Executing = false; + args.Handled = true; + + if (internalMsg != null && externalMsg != null) + { + ShowExecutionInternalPopup(internalMsg, attacker, victim, uid); + ShowExecutionExternalPopup(externalMsg, attacker, victim, uid); + } + } + + private void OnGetMeleeDamage(EntityUid uid, ExecutionComponent comp, ref GetMeleeDamageEvent args) + { + if (!TryComp(uid, out var melee) || + !TryComp(uid, out var execComp) || + !execComp.Executing) + { + return; + } + + var bonus = melee.Damage * execComp.DamageModifier - melee.Damage; + args.Damage += bonus; + } + + private void ShowExecutionInternalPopup(string locString, + EntityUid attacker, EntityUid victim, EntityUid weapon, bool predict = true) + { + if (predict) + { + _popupSystem.PopupClient( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + attacker, + PopupType.Medium + ); + } + else + { + _popupSystem.PopupEntity( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + Filter.Entities(attacker), + true, + PopupType.Medium + ); + } + + } + + private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon) + { + _popupSystem.PopupEntity( + Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), + attacker, + Filter.PvsExcept(attacker), + true, + PopupType.MediumCaution + ); + } +} diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index f57e873653..027dc03939 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -34,7 +34,6 @@ public abstract partial class SharedProjectileSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(PreventCollision); - SubscribeLocalEvent(AfterProjectileHit); SubscribeLocalEvent(OnEmbedProjectileHit); SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate); @@ -163,18 +162,6 @@ public abstract partial class SharedProjectileSystem : EntitySystem { args.Cancel("pacified-cannot-throw-embed"); } - - /// - /// Checks if the projectile is allowed to penetrate the target it hit. - /// - private void AfterProjectileHit(EntityUid uid, ProjectileComponent component, ref AfterProjectileHitEvent args) - { - //Overrides the original DeleteOnCollide if the projectile passes all penetration checks. - //This is to prevent having to set DeleteOnCollide to false on every prototype - //you want to give the ability to penetrate entities. - if(component.DeleteOnCollide) - component.DeleteOnCollide = false; - } } [Serializable, NetSerializable] @@ -206,4 +193,4 @@ public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target /// Raised after a projectile has dealt it's damage. /// [ByRefEvent] -public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, Fixture Fixture); +public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target); diff --git a/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs b/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs index 40925ad614..6325d95330 100644 --- a/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs +++ b/Content.Shared/Weapons/Ranged/Events/ShotAttemptedEvent.cs @@ -19,7 +19,7 @@ public record struct ShotAttemptedEvent public bool Cancelled { get; private set; } - /// + /// /// Prevent the gun from shooting /// public void Cancel() @@ -27,7 +27,7 @@ public record struct ShotAttemptedEvent Cancelled = true; } - /// + /// /// Allow the gun to shoot again, only use if you know what you are doing /// public void Uncancel() diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index ba22ba2cdc..2132dd2631 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -21,6 +21,7 @@ using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; +using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -143,7 +144,7 @@ public abstract partial class SharedGunSystem : EntitySystem gun.ShootCoordinates = GetCoordinates(msg.Coordinates); Log.Debug($"Set shoot coordinates to {gun.ShootCoordinates}"); - AttemptShoot(user.Value, ent, gun); + AttemptShootInternal(user.Value, ent, gun); } private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args) @@ -207,13 +208,38 @@ public abstract partial class SharedGunSystem : EntitySystem Dirty(uid, gun); } + /// + /// Attempts to shoot the specified target directly. + /// This may bypass projectiles firing etc. + /// + public bool AttemptDirectShoot(EntityUid user, EntityUid gunUid, EntityUid target, GunComponent gun) + { + // Unique name so people don't think it's "shoot towards" and not "I will teleport a bullet into them". + gun.ShootCoordinates = Transform(target).Coordinates; + + if (!TryTakeAmmo(user, gunUid, gun, out _, out _, out var args)) + { + gun.ShootCoordinates = null; + return false; + } + + var result = ShootDirect(gunUid, gun, target, args.Ammo, user: user); + gun.ShootCoordinates = null; + return result; + } + + protected virtual bool ShootDirect(EntityUid gunUid, GunComponent gun, EntityUid target, List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityUid user) + { + return false; + } + /// /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. /// public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates) { gun.ShootCoordinates = toCoordinates; - AttemptShoot(user, gunUid, gun); + AttemptShootInternal(user, gunUid, gun); gun.ShotCounter = 0; } @@ -224,20 +250,35 @@ public abstract partial class SharedGunSystem : EntitySystem { var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1)); gun.ShootCoordinates = coordinates; - AttemptShoot(gunUid, gunUid, gun); + AttemptShootInternal(gunUid, gunUid, gun); gun.ShotCounter = 0; } - private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun) + private void AttemptShootInternal(EntityUid user, EntityUid gunUid, GunComponent gun) + { + if (!TryTakeAmmo(user, gunUid, gun, out var fromCoordinates, out var toCoordinates, out var args)) + return; + + Shoot(gunUid, gun, args.Ammo, fromCoordinates, toCoordinates, out var userImpulse, user: user); + + if (userImpulse && TryComp(user, out var userPhysics)) + { + if (_gravity.IsWeightless(user, userPhysics)) + CauseImpulse(fromCoordinates, toCoordinates, user, userPhysics); + } + } + + /// + /// Validates if a gun can currently shoot. + /// + [Pure] + private bool CanShoot(EntityUid user, EntityUid gunUid, GunComponent gun) { if (gun.FireRateModified <= 0f || !_actionBlockerSystem.CanAttack(user)) - return; - - var toCoordinates = gun.ShootCoordinates; - - if (toCoordinates == null) - return; + { + return false; + } var curTime = Timing.CurTime; @@ -249,17 +290,42 @@ public abstract partial class SharedGunSystem : EntitySystem }; RaiseLocalEvent(gunUid, ref prevention); if (prevention.Cancelled) - return; + return false; RaiseLocalEvent(user, ref prevention); if (prevention.Cancelled) - return; + return false; // Need to do this to play the clicking sound for empty automatic weapons // but not play anything for burst fire. if (gun.NextFire > curTime) - return; + return false; + return true; + } + + /// + /// Tries to return ammo prepped for shooting if a gun is available to shoot. + /// + private bool TryTakeAmmo( + EntityUid user, + EntityUid gunUid, GunComponent gun, + out EntityCoordinates fromCoordinates, + out EntityCoordinates toCoordinates, + [NotNullWhen(true)] out TakeAmmoEvent? args) + { + toCoordinates = EntityCoordinates.Invalid; + fromCoordinates = EntityCoordinates.Invalid; + args = null; + + if (!CanShoot(user, gunUid, gun)) + return false; + + if (gun.ShootCoordinates == null) + return false; + + toCoordinates = gun.ShootCoordinates.Value; + var curTime = Timing.CurTime; var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); // First shot @@ -307,10 +373,11 @@ public abstract partial class SharedGunSystem : EntitySystem } gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); - return; + return false; } - var fromCoordinates = Transform(user).Coordinates; + fromCoordinates = Transform(user).Coordinates; + // Remove ammo var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user); @@ -345,24 +412,18 @@ public abstract partial class SharedGunSystem : EntitySystem // May cause prediction issues? Needs more tweaking gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); - return; + return false; } - return; + return false; } // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). - Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); var shotEv = new GunShotEvent(user, ev.Ammo); RaiseLocalEvent(gunUid, ref shotEv); - if (userImpulse && TryComp(user, out var userPhysics)) - { - if (_gravity.IsWeightless(user, userPhysics)) - CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); - } - - Dirty(gunUid, gun); + args = ev; + return true; } public void Shoot( diff --git a/Resources/Locale/en-US/execution/execution.ftl b/Resources/Locale/en-US/execution/execution.ftl index 8bdf326166..5bd4613e8c 100644 --- a/Resources/Locale/en-US/execution/execution.ftl +++ b/Resources/Locale/en-US/execution/execution.ftl @@ -14,17 +14,7 @@ execution-popup-gun-clumsy-internal = You miss {$victim}'s head and shoot your f execution-popup-gun-clumsy-external = {$attacker} misses {$victim} and shoots {POSS-ADJ($attacker)} foot instead! execution-popup-gun-empty = {CAPITALIZE(THE($weapon))} clicks. -suicide-popup-gun-initial-internal = You place the muzzle of {THE($weapon)} in your mouth. -suicide-popup-gun-initial-external = {$attacker} places the muzzle of {THE($weapon)} in {POSS-ADJ($attacker)} mouth. -suicide-popup-gun-complete-internal = You shoot yourself in the head! -suicide-popup-gun-complete-external = {$attacker} shoots {REFLEXIVE($attacker)} in the head! - execution-popup-melee-initial-internal = You ready {THE($weapon)} against {$victim}'s throat. execution-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against the throat of {$victim}. execution-popup-melee-complete-internal = You slit the throat of {$victim}! execution-popup-melee-complete-external = {$attacker} slits the throat of {$victim}! - -suicide-popup-melee-initial-internal = You ready {THE($weapon)} against your throat. -suicide-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against {POSS-ADJ($attacker)} throat. -suicide-popup-melee-complete-internal = You slit your throat with {THE($weapon)}! -suicide-popup-melee-complete-external = {$attacker} slits {POSS-ADJ($attacker)} throat with {THE($weapon)}! \ No newline at end of file diff --git a/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl b/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl index ff28cc44db..4a83cd455d 100644 --- a/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl +++ b/Resources/Locale/en-US/kitchen/components/butcherable-component.ftl @@ -1,4 +1,4 @@ -butcherable-different-tool = You are going to need a different tool to butcher { THE($target) }. +butcherable-different-tool = You need a different tool to butcher { THE($target) }. butcherable-knife-butchered-success = You butcher { THE($target) } with { THE($knife) }. butcherable-need-knife = Use a sharp object to butcher { THE($target) }. butcherable-not-in-container = { CAPITALIZE(THE($target)) } can't be in a container. diff --git a/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml b/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml index 62468b4615..a86965f96f 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml @@ -6,6 +6,8 @@ description: A small piece of crystal. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite layers: - sprite: Objects/Materials/Shards/crystal.rsi diff --git a/Resources/Prototypes/Entities/Objects/Materials/shards.yml b/Resources/Prototypes/Entities/Objects/Materials/shards.yml index fa57f90c18..22d5cdefe4 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/shards.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/shards.yml @@ -5,6 +5,8 @@ description: It's a shard of some unknown material. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite layers: - sprite: Objects/Materials/Shards/shard.rsi diff --git a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml index b7c73f5e0c..98bc92968b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml @@ -5,6 +5,8 @@ description: In Space Glasgow this is called a conversation starter. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: MeleeWeapon attackRate: 1.5 damage: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml index 9d685e1ddc..236ed20681 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/HMGs/hmgs.yml @@ -19,6 +19,7 @@ path: /Audio/Weapons/Guns/Empty/lmg_empty.ogg - type: StaticPrice price: 500 + - type: Execution # No chamber because HMG may want its own - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml index 49b2eeaada..499a950e78 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml @@ -60,6 +60,7 @@ price: 500 - type: UseDelay delay: 1 + - type: Execution - type: entity name: L6 SAW diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index 8b31bf40ed..80de02c6da 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -19,6 +19,7 @@ containers: ballistic-ammo: !type:Container ents: [] + - type: Execution - type: entity name: china lake diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml index 9507eada3b..2b2c6fe955 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml @@ -65,6 +65,7 @@ - type: Appearance - type: StaticPrice price: 500 + - type: Execution - type: entity name: viper @@ -199,7 +200,7 @@ name: N1984 parent: BaseWeaponPistol id: WeaponPistolN1984 # the spaces in description are for formatting. - description: The sidearm of any self respecting officer. Comes in .45 magnum, the lord's caliber. + description: The sidearm of any self respecting officer. Comes in .45 magnum, the lord's caliber. components: - type: Sprite sprite: Objects/Weapons/Guns/Pistols/N1984.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml index 5bc8125eba..93e8b1a669 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml @@ -49,6 +49,7 @@ gun_chamber: !type:ContainerSlot - type: StaticPrice price: 500 + - type: Execution - type: entity name: AKMS diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml index b693bdba37..ea82be46bc 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml @@ -54,6 +54,7 @@ gun_chamber: !type:ContainerSlot - type: StaticPrice price: 500 + - type: Execution - type: entity name: Atreides diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml index f43df5f37c..70ed96876e 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml @@ -42,6 +42,7 @@ ents: [] - type: StaticPrice price: 500 + - type: Execution - type: entity name: Bulldog @@ -98,6 +99,7 @@ - type: Appearance - type: StaticPrice price: 500 + - type: Execution - type: entity name: double-barreled shotgun diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml index 753f109a17..6640e98f5f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml @@ -36,6 +36,7 @@ ents: [] - type: StaticPrice price: 500 + - type: Execution - type: entity name: Kardashev-Mosin diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml index cfcc0a01cd..2a07fd4fe0 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml @@ -36,3 +36,4 @@ quickEquip: false slots: - Belt + - type: Execution diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml index ae1f5df3c1..add776422d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml @@ -107,6 +107,7 @@ containers: storagebase: !type:Container ents: [] + - type: Execution # shoots bullets instead of throwing them, no other changes - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index 497876f359..267e3a7891 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -5,6 +5,8 @@ description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter. components: - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite sprite: Objects/Weapons/Melee/armblade.rsi state: icon diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml index b46ee09633..aadb994244 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml @@ -8,6 +8,8 @@ tags: - FireAxe - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Sprite sprite: Objects/Weapons/Melee/fireaxe.rsi state: icon diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml index 106e8bf440..432da5efe9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml @@ -7,6 +7,8 @@ tags: - Knife - type: Sharp + - type: Execution + doAfterDuration: 4.0 - type: Utensil types: - Knife diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 05cac3ae7b..d27c6c6883 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -1,10 +1,21 @@ +- type: entity + name: Sword + parent: BaseItem + id: BaseSword + description: A sharp sword. + abstract: true + components: + - type: Sharp + - type: Execution + doAfterDuration: 4.0 + - type: DisarmMalus + - type: entity name: captain's sabre - parent: BaseItem + parent: BaseSword id: CaptainSabre description: A ceremonial weapon belonging to the captain of the station. components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/captain_sabre.rsi state: icon @@ -26,15 +37,13 @@ - type: Tag tags: - CaptainSabre - - type: DisarmMalus - type: entity name: katana - parent: BaseItem + parent: BaseSword id: Katana description: Ancient craftwork made with not so ancient plasteel. components: - - type: Sharp - type: Tag tags: - Katana @@ -51,7 +60,6 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/katana.rsi - - type: DisarmMalus - type: entity name: energy katana @@ -86,11 +94,10 @@ - type: entity name: machete - parent: BaseItem + parent: BaseSword id: Machete description: A large, vicious looking blade. components: - - type: Sharp - type: Tag tags: - Machete @@ -107,15 +114,13 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/machete.rsi - - type: DisarmMalus - type: entity name: claymore - parent: BaseItem + parent: BaseSword id: Claymore description: An ancient war blade. components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/claymore.rsi state: icon @@ -133,15 +138,13 @@ sprite: Objects/Weapons/Melee/claymore.rsi slots: - back - - type: DisarmMalus - type: entity name: cutlass - parent: BaseItem + parent: BaseSword id: Cutlass description: A wickedly curved blade, often seen in the hands of space pirates. components: - - type: Sharp - type: Tag tags: - Machete @@ -158,15 +161,13 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/cutlass.rsi - - type: DisarmMalus - type: entity name: The Throngler - parent: BaseItem + parent: BaseSword id: Throngler description: Why would you make this? components: - - type: Sharp - type: Sprite sprite: Objects/Weapons/Melee/Throngler2.rsi state: icon @@ -190,4 +191,3 @@ - type: Item size: Ginormous sprite: Objects/Weapons/Melee/Throngler-in-hand.rsi - - type: DisarmMalus