Revert "Revert "Cleanup ExecutionSystem (#24382)" (#25555)"

This reverts commit bb0776c496.

# Conflicts:
#	Content.Server/Projectiles/ProjectileSystem.cs
#	Content.Shared/Projectiles/SharedProjectileSystem.cs
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/flare_gun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
This commit is contained in:
Remuchi
2024-03-27 19:42:57 +07:00
parent c04b962141
commit 3c9c149b81
28 changed files with 678 additions and 114 deletions

View File

@@ -115,10 +115,10 @@ public sealed class ExecutionSystem : EntitySystem
args.Verbs.Add(verb); args.Verbs.Add(verb);
} }
private bool CanExecuteWithAny(EntityUid weapon, EntityUid victim, EntityUid attacker) private bool CanExecuteWithAny(EntityUid victim, EntityUid attacker)
{ {
// No point executing someone if they can't take damage // No point executing someone if they can't take damage
if (!TryComp<DamageableComponent>(victim, out var damage)) if (!TryComp<DamageableComponent>(victim, out _))
return false; return false;
// You can't execute something that cannot die // You can't execute something that cannot die
@@ -143,7 +143,7 @@ public sealed class ExecutionSystem : EntitySystem
private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user) private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user)
{ {
if (!CanExecuteWithAny(weapon, victim, user)) return false; if (!CanExecuteWithAny(victim, user)) return false;
// We must be able to actually hurt people with the weapon // We must be able to actually hurt people with the weapon
if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f) if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f)
@@ -154,7 +154,7 @@ public sealed class ExecutionSystem : EntitySystem
private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user) private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user)
{ {
if (!CanExecuteWithAny(weapon, victim, user)) return false; if (!CanExecuteWithAny(victim, user)) return false;
// We must be able to actually fire the gun // We must be able to actually fire the gun
if (!TryComp<GunComponent>(weapon, out var gun) && _gunSystem.CanShoot(gun!)) if (!TryComp<GunComponent>(weapon, out var gun) && _gunSystem.CanShoot(gun!))
@@ -172,23 +172,28 @@ public sealed class ExecutionSystem : EntitySystem
if (attacker == victim) if (attacker == victim)
{ {
ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
else else
{ {
ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
var doAfter = var doAfter = new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon,
new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) target: victim, used: weapon)
{ {
BreakOnTargetMove = true, BreakOnMove = true,
BreakOnUserMove = true, BreakOnDamage = true,
BreakOnDamage = true, NeedHand = true
NeedHand = true };
};
_doAfterSystem.TryStartDoAfter(doAfter); _doAfterSystem.TryStartDoAfter(doAfter);
} }
@@ -200,39 +205,31 @@ public sealed class ExecutionSystem : EntitySystem
if (attacker == victim) if (attacker == victim)
{ {
ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
else else
{ {
ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
var doAfter = var doAfter = new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon,
new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon) target: victim, used: weapon)
{ {
BreakOnTargetMove = true, BreakOnDamage = true,
BreakOnUserMove = true, NeedHand = true
BreakOnDamage = true, };
NeedHand = true
};
_doAfterSystem.TryStartDoAfter(doAfter); _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) private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEvent args)
{ {
if (args.Handled || args.Cancelled || args.Used == null || args.Target == null) if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
@@ -252,13 +249,19 @@ public sealed class ExecutionSystem : EntitySystem
if (attacker == victim) if (attacker == victim)
{ {
ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
else else
{ {
ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker),
PopupType.MediumCaution, attacker, victim, weapon);
} }
} }
@@ -311,7 +314,9 @@ public sealed class ExecutionSystem : EntitySystem
if (ev.Ammo.Count <= 0) if (ev.Ammo.Count <= 0)
{ {
_audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default); _audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default);
ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim,
weapon);
return; return;
} }
@@ -325,7 +330,9 @@ public sealed class ExecutionSystem : EntitySystem
case CartridgeAmmoComponent cartridge: case CartridgeAmmoComponent cartridge:
// Get the damage value // Get the damage value
var prototype = _prototypeManager.Index<EntityPrototype>(cartridge.Prototype); var prototype = _prototypeManager.Index<EntityPrototype>(cartridge.Prototype);
prototype.TryGetComponent<ProjectileComponent>(out var projectileA, _componentFactory); // sloth forgive me prototype.TryGetComponent<ProjectileComponent>(out var projectileA,
_componentFactory); // sloth forgive me
if (projectileA != null) if (projectileA != null)
{ {
damage = projectileA.Damage * cartridge.Count; damage = projectileA.Damage * cartridge.Count;
@@ -344,6 +351,7 @@ public sealed class ExecutionSystem : EntitySystem
{ {
damage = projectileB.Damage; damage = projectileB.Damage;
} }
Del(ammoUid); Del(ammoUid);
break; break;
@@ -360,8 +368,11 @@ public sealed class ExecutionSystem : EntitySystem
{ {
if (_interactionSystem.TryRollClumsy(attacker, 0.33333333f, clumsy)) 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-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("execution-popup-gun-clumsy-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon); 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) // You shoot yourself with the gun (no damage multiplier)
_damageableSystem.TryChangeDamage(attacker, damage, origin: attacker); _damageableSystem.TryChangeDamage(attacker, damage, origin: attacker);
@@ -377,18 +388,29 @@ public sealed class ExecutionSystem : EntitySystem
// Popups // Popups
if (attacker != victim) if (attacker != victim)
{ {
ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon); ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium,
ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); attacker, victim, weapon);
ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker),
PopupType.LargeCaution, attacker, victim, weapon);
} }
else else
{ {
ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon); ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution,
ShowExecutionPopup("suicide-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon); 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, private void ShowExecutionPopup(
EntityUid attacker, EntityUid victim, EntityUid weapon) string locString,
Filter filter,
PopupType type,
EntityUid attacker,
EntityUid victim,
EntityUid weapon)
{ {
_popupSystem.PopupEntity(Loc.GetString( _popupSystem.PopupEntity(Loc.GetString(
locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)), locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),

View File

@@ -9,9 +9,6 @@ using Content.Shared._White;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
using Content.Shared.Mobs.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
namespace Content.Server.Projectiles; namespace Content.Server.Projectiles;
@@ -64,12 +61,27 @@ public sealed class ProjectileSystem : SharedProjectileSystem
return; return;
} }
if (TryHandleProjectile(target, (uid, component)))
{
var direction = args.OurBody.LinearVelocity.Normalized();
_sharedCameraRecoil.KickCamera(target, direction);
}
}
/// <summary>
/// Tries to handle a projectile interacting with the target.
/// </summary>
/// <returns>True if the target isn't deleted.</returns>
public bool TryHandleProjectile(EntityUid target, Entity<ProjectileComponent> projectile)
{
var uid = projectile.Owner;
var component = projectile.Comp;
var ev = new ProjectileHitEvent(component.Damage, target, component.Shooter); var ev = new ProjectileHitEvent(component.Damage, target, component.Shooter);
RaiseLocalEvent(uid, ref ev); RaiseLocalEvent(uid, ref ev);
var otherName = ToPrettyString(target); var otherName = ToPrettyString(target);
var direction = args.OurBody.LinearVelocity.Normalized(); var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, origin: component.Shooter);
var modifiedDamage = _damageableSystem.TryChangeDamage(target, ev.Damage * DamageModifier, component.IgnoreResistances, origin: component.Shooter);
var deleted = Deleted(target); var deleted = Deleted(target);
if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter)) if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter))
@@ -87,11 +99,13 @@ public sealed class ProjectileSystem : SharedProjectileSystem
if (!deleted) if (!deleted)
{ {
_guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound); _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound);
_sharedCameraRecoil.KickCamera(target, direction);
} }
component.DamagedEntity = true; component.DamagedEntity = true;
var afterProjectileHitEvent = new AfterProjectileHitEvent(component.Damage, target);
RaiseLocalEvent(uid, ref afterProjectileHitEvent);
if (component.DeleteOnCollide) if (component.DeleteOnCollide)
QueueDel(uid); QueueDel(uid);
@@ -99,5 +113,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem
{ {
RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager)); RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
} }
return !deleted;
} }
} }

View File

@@ -4,6 +4,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Cargo.Systems; using Content.Server.Cargo.Systems;
using Content.Server.Interaction; using Content.Server.Interaction;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Projectiles;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Components; using Content.Server.Weapons.Ranged.Components;
using Content.Server._White.Crossbow; using Content.Server._White.Crossbow;
@@ -32,13 +33,13 @@ namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem public sealed partial class GunSystem : SharedGunSystem
{ {
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly ProjectileSystem _projectile = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly StunSystem _stun = default!;
@@ -69,6 +70,137 @@ public sealed partial class GunSystem : SharedGunSystem
args.Price += price * component.UnspawnedCount; 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<EntityUid>(ammo.Count);
var cartridgeBullets = new List<EntityUid>();
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<EntityUid>() { 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, 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) EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false)
{ {
@@ -76,7 +208,7 @@ public sealed partial class GunSystem : SharedGunSystem
// Try a clumsy roll // Try a clumsy roll
// TODO: Who put this here // TODO: Who put this here
if (TryComp<ClumsyComponent>(user, out var clumsy) && gun.ClumsyProof == false) if (TryComp<ClumsyComponent>(user, out var clumsy) && !gun.ClumsyProof)
{ {
for (var i = 0; i < ammo.Count; i++) for (var i = 0; i < ammo.Count; i++)
{ {
@@ -99,6 +231,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 fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
var mapDirection = toMap - fromMap.Position; var mapDirection = toMap - fromMap.Position;
@@ -106,7 +240,7 @@ public sealed partial class GunSystem : SharedGunSystem
var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
// If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. // 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) ? fromCoordinates.WithEntityId(gridUid, EntityManager)
: new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position); : new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
@@ -125,6 +259,7 @@ public sealed partial class GunSystem : SharedGunSystem
// I must be high because this was getting tripped even when true. // I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero); // DebugTools.Assert(direction != Vector2.Zero);
var shotProjectiles = new List<EntityUid>(ammo.Count); var shotProjectiles = new List<EntityUid>(ammo.Count);
var cartridgeBullets = new List<EntityUid>();
foreach (var (ent, shootable) in ammo) foreach (var (ent, shootable) in ammo)
{ {
@@ -153,21 +288,23 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
var uid = Spawn(cartridge.Prototype, fromEnt); var uid = Spawn(cartridge.Prototype, fromEnt);
ShootOrThrow(uid, angles[i].ToVec(), gunVelocity, gun, gunUid, user); ShootOrThrow(uid, angles[i].ToVec(), gunVelocity, gun, gunUid, user);
shotProjectiles.Add(uid); cartridgeBullets.Add(uid);
} }
} }
else else
{ {
var uid = Spawn(cartridge.Prototype, fromEnt); var uid = Spawn(cartridge.Prototype, fromEnt);
ShootOrThrow(uid, mapDirection, gunVelocity, gun, gunUid, user); ShootOrThrow(uid, mapDirection, gunVelocity, gun, gunUid, user);
shotProjectiles.Add(uid); cartridgeBullets.Add(uid);
} }
RaiseLocalEvent(ent!.Value, new AmmoShotEvent() RaiseLocalEvent(ent!.Value, new AmmoShotEvent()
{ {
FiredProjectiles = shotProjectiles, FiredProjectiles = cartridgeBullets,
}); });
shotProjectiles.AddRange(cartridgeBullets);
cartridgeBullets.Clear();
SetCartridgeSpent(ent.Value, cartridge, true); SetCartridgeSpent(ent.Value, cartridge, true);
MuzzleFlash(gunUid, cartridge, user); MuzzleFlash(gunUid, cartridge, user);
Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user); Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);

View File

@@ -0,0 +1,9 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Execution;
[Serializable, NetSerializable]
public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -0,0 +1,26 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Execution;
/// <summary>
/// Added to entities that can be used to execute another target.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ExecutionComponent : Component
{
/// <summary>
/// How long the execution duration lasts.
/// </summary>
[DataField, AutoNetworkedField]
public float DoAfterDuration = 5f;
[DataField, AutoNetworkedField]
public float DamageModifier = 9f;
// Not networked because this is transient inside of a tick.
/// <summary>
/// True if it is currently executing for handlers.
/// </summary>
[DataField]
public bool Executing = true;
}

View File

@@ -0,0 +1,244 @@
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.Player;
namespace Content.Shared.Execution;
/// <summary>
/// Verb for violently murdering cuffed creatures.
/// </summary>
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";
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs);
SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter);
SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
}
private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent<UtilityVerb> 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<GunComponent>(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)
{
BreakOnMove = 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<DamageableComponent>(victim, out _))
return false;
// You can't execute something that cannot die
if (!TryComp<MobStateComponent>(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))
{
// 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<MeleeWeaponComponent>(uid, out var melee) ||
!TryComp<ExecutionComponent>(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
);
}
}

View File

@@ -303,3 +303,9 @@ public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, Projectile
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null); public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null);
/// <summary>
/// Raised after a projectile has dealt it's damage.
/// </summary>
[ByRefEvent]
public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target);

View File

@@ -19,7 +19,7 @@ public record struct ShotAttemptedEvent
public bool Cancelled { get; private set; } public bool Cancelled { get; private set; }
/// </summary> /// <summary>
/// Prevent the gun from shooting /// Prevent the gun from shooting
/// </summary> /// </summary>
public void Cancel() public void Cancel()
@@ -27,7 +27,7 @@ public record struct ShotAttemptedEvent
Cancelled = true; Cancelled = true;
} }
/// </summary> /// <summary>
/// Allow the gun to shoot again, only use if you know what you are doing /// Allow the gun to shoot again, only use if you know what you are doing
/// </summary> /// </summary>
public void Uncancel() public void Uncancel()

View File

@@ -21,6 +21,7 @@ using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -140,7 +141,7 @@ public abstract partial class SharedGunSystem : EntitySystem
gun.ShootCoordinates = GetCoordinates(msg.Coordinates); gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
gun.Target = GetEntity(msg.Target); gun.Target = GetEntity(msg.Target);
Log.Debug($"Set shoot coordinates to {gun.ShootCoordinates}"); 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) private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
@@ -204,13 +205,38 @@ public abstract partial class SharedGunSystem : EntitySystem
Dirty(uid, gun); Dirty(uid, gun);
} }
/// <summary>
/// Attempts to shoot the specified target directly.
/// This may bypass projectiles firing etc.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
/// </summary> /// </summary>
public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates) public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates)
{ {
gun.ShootCoordinates = toCoordinates; gun.ShootCoordinates = toCoordinates;
AttemptShoot(user, gunUid, gun); AttemptShootInternal(user, gunUid, gun);
gun.ShotCounter = 0; gun.ShotCounter = 0;
} }
@@ -221,20 +247,35 @@ public abstract partial class SharedGunSystem : EntitySystem
{ {
var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1)); var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1));
gun.ShootCoordinates = coordinates; gun.ShootCoordinates = coordinates;
AttemptShoot(gunUid, gunUid, gun); AttemptShootInternal(gunUid, gunUid, gun);
gun.ShotCounter = 0; 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<PhysicsComponent>(user, out var userPhysics))
{
if (_gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates, user, userPhysics);
}
}
/// <summary>
/// Validates if a gun can currently shoot.
/// </summary>
[Pure]
private bool CanShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
{ {
if (gun.FireRateModified <= 0f || if (gun.FireRateModified <= 0f ||
!_actionBlockerSystem.CanAttack(user)) !_actionBlockerSystem.CanAttack(user))
return; {
return false;
var toCoordinates = gun.ShootCoordinates; }
if (toCoordinates == null)
return;
var curTime = Timing.CurTime; var curTime = Timing.CurTime;
@@ -246,17 +287,42 @@ public abstract partial class SharedGunSystem : EntitySystem
}; };
RaiseLocalEvent(gunUid, ref prevention); RaiseLocalEvent(gunUid, ref prevention);
if (prevention.Cancelled) if (prevention.Cancelled)
return; return false;
RaiseLocalEvent(user, ref prevention); RaiseLocalEvent(user, ref prevention);
if (prevention.Cancelled) if (prevention.Cancelled)
return; return false;
// Need to do this to play the clicking sound for empty automatic weapons // Need to do this to play the clicking sound for empty automatic weapons
// but not play anything for burst fire. // but not play anything for burst fire.
if (gun.NextFire > curTime) if (gun.NextFire > curTime)
return; return false;
return true;
}
/// <summary>
/// Tries to return ammo prepped for shooting if a gun is available to shoot.
/// </summary>
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); var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
// First shot // First shot
@@ -308,10 +374,11 @@ public abstract partial class SharedGunSystem : EntitySystem
} }
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); 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 // Remove ammo
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user); var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
@@ -346,24 +413,18 @@ public abstract partial class SharedGunSystem : EntitySystem
// May cause prediction issues? Needs more tweaking // May cause prediction issues? Needs more tweaking
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); 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 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); var shotEv = new GunShotEvent(user, ev.Ammo);
RaiseLocalEvent(gunUid, ref shotEv); RaiseLocalEvent(gunUid, ref shotEv);
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics)) args = ev;
{ return true;
if (_gravity.IsWeightless(user, userPhysics))
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
}
Dirty(gunUid, gun);
} }
public void Shoot( public void Shoot(

View File

@@ -0,0 +1,20 @@
execution-verb-name = Execute
execution-verb-message = Use your weapon to execute someone.
# All the below localisation strings have access to the following variables
# attacker (the person committing the execution)
# victim (the person being executed)
# weapon (the weapon used for the execution)
execution-popup-gun-initial-internal = You ready the muzzle of {THE($weapon)} against {$victim}'s head.
execution-popup-gun-initial-external = {$attacker} readies the muzzle of {THE($weapon)} against {$victim}'s head.
execution-popup-gun-complete-internal = You blast {$victim} in the head!
execution-popup-gun-complete-external = {$attacker} blasts {$victim} in the head!
execution-popup-gun-clumsy-internal = You miss {$victim}'s head and shoot your foot instead!
execution-popup-gun-clumsy-external = {$attacker} misses {$victim} and shoots {POSS-ADJ($attacker)} foot instead!
execution-popup-gun-empty = {CAPITALIZE(THE($weapon))} clicks.
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}!

View File

@@ -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-knife-butchered-success = You butcher { THE($target) } with { THE($knife) }.
butcherable-need-knife = Use a sharp object to butcher { THE($target) }. butcherable-need-knife = Use a sharp object to butcher { THE($target) }.
butcherable-not-in-container = { CAPITALIZE(THE($target)) } can't be in a container. butcherable-not-in-container = { CAPITALIZE(THE($target)) } can't be in a container.

View File

@@ -6,6 +6,8 @@
description: A small piece of crystal. description: A small piece of crystal.
components: components:
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite - type: Sprite
layers: layers:
- sprite: Objects/Materials/Shards/crystal.rsi - sprite: Objects/Materials/Shards/crystal.rsi

View File

@@ -5,6 +5,8 @@
description: It's a shard of some unknown material. description: It's a shard of some unknown material.
components: components:
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite - type: Sprite
layers: layers:
- sprite: Objects/Materials/Shards/shard.rsi - sprite: Objects/Materials/Shards/shard.rsi

View File

@@ -5,6 +5,8 @@
description: In Space Glasgow this is called a conversation starter. description: In Space Glasgow this is called a conversation starter.
components: components:
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: MeleeWeapon - type: MeleeWeapon
attackRate: 1.5 attackRate: 1.5
damage: damage:

View File

@@ -19,6 +19,7 @@
path: /Audio/Weapons/Guns/Empty/lmg_empty.ogg path: /Audio/Weapons/Guns/Empty/lmg_empty.ogg
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
# No chamber because HMG may want its own # No chamber because HMG may want its own
- type: entity - type: entity

View File

@@ -60,6 +60,7 @@
price: 500 price: 500
- type: UseDelay - type: UseDelay
delay: 1 delay: 1
- type: Execution
- type: entity - type: entity
name: L6 SAW name: L6 SAW

View File

@@ -19,6 +19,7 @@
containers: containers:
ballistic-ammo: !type:Container ballistic-ammo: !type:Container
ents: [] ents: []
- type: Execution
- type: entity - type: entity
name: china lake name: china lake

View File

@@ -65,6 +65,7 @@
- type: Appearance - type: Appearance
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: viper name: viper

View File

@@ -49,6 +49,7 @@
gun_chamber: !type:ContainerSlot gun_chamber: !type:ContainerSlot
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: AKMS name: AKMS

View File

@@ -54,6 +54,7 @@
gun_chamber: !type:ContainerSlot gun_chamber: !type:ContainerSlot
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: Atreides name: Atreides

View File

@@ -43,6 +43,7 @@
ents: [] ents: []
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: Bulldog name: Bulldog
@@ -99,6 +100,7 @@
- type: Appearance - type: Appearance
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: double-barreled shotgun name: double-barreled shotgun

View File

@@ -38,6 +38,7 @@
ents: [] ents: []
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: Execution
- type: entity - type: entity
name: Kardashev-Mosin name: Kardashev-Mosin

View File

@@ -37,3 +37,4 @@
slots: slots:
- Belt - Belt
- suitStorage - suitStorage
- type: Execution

View File

@@ -107,6 +107,7 @@
containers: containers:
storagebase: !type:Container storagebase: !type:Container
ents: [] ents: []
- type: Execution
# shoots bullets instead of throwing them, no other changes # shoots bullets instead of throwing them, no other changes
- type: entity - type: entity

View File

@@ -5,6 +5,8 @@
description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter. description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter.
components: components:
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite - type: Sprite
sprite: Objects/Weapons/Melee/armblade.rsi sprite: Objects/Weapons/Melee/armblade.rsi
state: icon state: icon

View File

@@ -8,6 +8,8 @@
tags: tags:
- FireAxe - FireAxe
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Sprite - type: Sprite
sprite: Objects/Weapons/Melee/fireaxe.rsi sprite: Objects/Weapons/Melee/fireaxe.rsi
state: icon state: icon

View File

@@ -7,6 +7,8 @@
tags: tags:
- Knife - Knife
- type: Sharp - type: Sharp
- type: Execution
doAfterDuration: 4.0
- type: Utensil - type: Utensil
types: types:
- Knife - Knife

View File

@@ -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 - type: entity
name: captain's sabre name: captain's sabre
parent: BaseItem parent: BaseSword
id: CaptainSabre id: CaptainSabre
description: A ceremonial weapon belonging to the captain of the station. description: A ceremonial weapon belonging to the captain of the station.
components: components:
- type: Sharp
- type: Sprite - type: Sprite
sprite: Objects/Weapons/Melee/captain_sabre.rsi sprite: Objects/Weapons/Melee/captain_sabre.rsi
state: icon state: icon
@@ -27,15 +38,13 @@
- type: Tag - type: Tag
tags: tags:
- CaptainSabre - CaptainSabre
- type: DisarmMalus
- type: entity - type: entity
name: katana name: katana
parent: BaseItem parent: BaseSword
id: Katana id: Katana
description: Ancient craftwork made with not so ancient plasteel. description: Ancient craftwork made with not so ancient plasteel.
components: components:
- type: Sharp
- type: Tag - type: Tag
tags: tags:
- Katana - Katana
@@ -52,7 +61,6 @@
- type: Item - type: Item
size: Huge size: Huge
sprite: Objects/Weapons/Melee/katana.rsi sprite: Objects/Weapons/Melee/katana.rsi
- type: DisarmMalus
- type: entity - type: entity
name: energy katana name: energy katana
@@ -87,11 +95,10 @@
- type: entity - type: entity
name: machete name: machete
parent: BaseItem parent: BaseSword
id: Machete id: Machete
description: A large, vicious looking blade. description: A large, vicious looking blade.
components: components:
- type: Sharp
- type: Tag - type: Tag
tags: tags:
- Machete - Machete
@@ -108,7 +115,6 @@
- type: Item - type: Item
size: Large size: Large
sprite: Objects/Weapons/Melee/machete.rsi sprite: Objects/Weapons/Melee/machete.rsi
- type: DisarmMalus
- type: Construction - type: Construction
deconstructionTarget: null deconstructionTarget: null
graph: SwordGraph graph: SwordGraph
@@ -116,11 +122,10 @@
- type: entity - type: entity
name: claymore name: claymore
parent: BaseItem parent: BaseSword
id: Claymore id: Claymore
description: An ancient war blade. description: An ancient war blade.
components: components:
- type: Sharp
- type: Sprite - type: Sprite
sprite: Objects/Weapons/Melee/claymore.rsi sprite: Objects/Weapons/Melee/claymore.rsi
state: icon state: icon
@@ -138,7 +143,6 @@
sprite: Objects/Weapons/Melee/claymore.rsi sprite: Objects/Weapons/Melee/claymore.rsi
slots: slots:
- back - back
- type: DisarmMalus
- type: Construction - type: Construction
deconstructionTarget: null deconstructionTarget: null
graph: SwordGraph graph: SwordGraph
@@ -146,11 +150,10 @@
- type: entity - type: entity
name: cutlass name: cutlass
parent: BaseItem parent: BaseSword
id: Cutlass id: Cutlass
description: A wickedly curved blade, often seen in the hands of space pirates. description: A wickedly curved blade, often seen in the hands of space pirates.
components: components:
- type: Sharp
- type: Tag - type: Tag
tags: tags:
- Machete - Machete
@@ -167,15 +170,13 @@
- type: Item - type: Item
size: Large size: Large
sprite: Objects/Weapons/Melee/cutlass.rsi sprite: Objects/Weapons/Melee/cutlass.rsi
- type: DisarmMalus
- type: entity - type: entity
name: The Throngler name: The Throngler
parent: BaseItem parent: BaseSword
id: Throngler id: Throngler
description: Why would you make this? description: Why would you make this?
components: components:
- type: Sharp
- type: Sprite - type: Sprite
sprite: Objects/Weapons/Melee/Throngler2.rsi sprite: Objects/Weapons/Melee/Throngler2.rsi
state: icon state: icon
@@ -199,4 +200,3 @@
- type: Item - type: Item
size: Ginormous size: Ginormous
sprite: Objects/Weapons/Melee/Throngler-in-hand.rsi sprite: Objects/Weapons/Melee/Throngler-in-hand.rsi
- type: DisarmMalus