diff --git a/Content.Client/White/Crossbow/DrawableVisualizerSystem.cs b/Content.Client/White/Crossbow/DrawableVisualizerSystem.cs new file mode 100644 index 0000000000..ea30ff4016 --- /dev/null +++ b/Content.Client/White/Crossbow/DrawableVisualizerSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Weapons.Ranged.Systems; +using Content.Shared.White.Crossbow; +using Robust.Client.GameObjects; + +namespace Content.Client.White.Crossbow; + +public sealed class DrawableSystem : VisualizerSystem +{ + protected override void OnAppearanceChange(EntityUid uid, DrawableComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + var drawn = args.AppearanceData.TryGetValue(DrawableVisuals.Drawn, out var drawnObj) && drawnObj is true; + + var hasAmmo = args.AppearanceData.TryGetValue(AmmoVisuals.AmmoCount, out var ammoCount) && (int) ammoCount > 0; + + var state = drawn ? "drawn" : hasAmmo ? "loaded" : "base"; + args.Sprite.LayerSetState(0, state); + } +} diff --git a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs index c7600ec221..92008babca 100644 --- a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs +++ b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Damage.Components; using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Camera; +using Content.Server.White.Crossbow; using Content.Shared.Damage; using Content.Shared.Damage.Events; using Content.Shared.Damage.Systems; @@ -32,7 +33,17 @@ namespace Content.Server.Damage.Systems private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args) { - var dmg = _damageable.TryChangeDamage(args.Target, component.Damage, component.IgnoreResistances, origin: args.Component.Thrower); + // WD EDIT START + if (args.Handled) + return; + + var damage = component.Damage; + + if (TryComp(uid, out ThrowDamageModifierComponent? modifier)) + damage += modifier.Damage; + + var dmg = _damageable.TryChangeDamage(args.Target, damage, component.IgnoreResistances, origin: args.Component.Thrower); + // WD EDIT END // Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying. if (dmg != null && HasComp(args.Target)) diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index 798be3fc8e..c159fb36d3 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -1,3 +1,5 @@ +using Content.Server.Stack; +using Content.Shared.Stacks; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Map; @@ -6,6 +8,8 @@ namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { + [Dependency] private readonly StackSystem _stack = default!; + protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates) { EntityUid? ent = null; @@ -32,4 +36,9 @@ public sealed partial class GunSystem var cycledEvent = new GunCycledEvent(); RaiseLocalEvent(uid, ref cycledEvent); } + + protected override EntityUid GetStackEntity(EntityUid uid, StackComponent stack) // WD + { + return _stack.Split(uid, 1, Transform(uid).Coordinates, stack) ?? uid; + } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 5f8ab0ecb6..5bc82ca67e 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Interaction; using Content.Server.Power.EntitySystems; using Content.Server.Stunnable; using Content.Server.Weapons.Ranged.Components; +using Content.Server.White.Crossbow; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; @@ -13,6 +14,7 @@ using Content.Shared.Effects; using Content.Shared.FixedPoint; using Content.Shared.Interaction.Components; using Content.Shared.Projectiles; +using Content.Shared.Throwing; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged.Components; @@ -42,6 +44,7 @@ public sealed partial class GunSystem : SharedGunSystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly PoweredSystem _powered = default!; // WD public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation; public const float GunClumsyChance = 0.5f; @@ -306,7 +309,17 @@ public sealed partial class GunSystem : SharedGunSystem { RemoveShootable(uid); // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack. - ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeed, user); + // WD EDIT START + var coefficient = _powered.GetPowerCoefficient(gunUid); + if (gun.ForceThrowingAngle) + { + var angle = EnsureComp(uid); + angle.Angle = gun.Angle; + } + ThrowingSystem.TryThrow(uid, mapDirection.Normalized() * 7f * coefficient, gun.ProjectileSpeed, user); + if (gun.ForceThrowingAngle) + RemComp(uid); + // WD EDIT END return; } diff --git a/Content.Server/White/Crossbow/ModifyDamageOnShootComponent.cs b/Content.Server/White/Crossbow/ModifyDamageOnShootComponent.cs new file mode 100644 index 0000000000..de6d76b64f --- /dev/null +++ b/Content.Server/White/Crossbow/ModifyDamageOnShootComponent.cs @@ -0,0 +1,19 @@ +using System.Numerics; +using Content.Shared.Damage; + +namespace Content.Server.White.Crossbow; + +[RegisterComponent] +public sealed partial class ModifyDamageOnShootComponent : Component +{ + [DataField("damage", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier Damage = new(); + + [DataField("addEmbedding")] + [ViewVariables(VVAccess.ReadWrite)] + public bool AddEmbedding; + + [DataField("offset")] + public Vector2 Offset = Vector2.Zero; +} diff --git a/Content.Server/White/Crossbow/ModifyDamageOnShootSystem.cs b/Content.Server/White/Crossbow/ModifyDamageOnShootSystem.cs new file mode 100644 index 0000000000..ac39bcdd20 --- /dev/null +++ b/Content.Server/White/Crossbow/ModifyDamageOnShootSystem.cs @@ -0,0 +1,33 @@ +using Content.Shared.Projectiles; +using Content.Shared.Throwing; +using Content.Shared.Weapons.Ranged.Events; + +namespace Content.Server.White.Crossbow; + +public sealed class ModifyDamageOnShootSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnShoot); + } + + private void OnShoot(EntityUid uid, ModifyDamageOnShootComponent component, AmmoShotEvent args) + { + foreach (var proj in args.FiredProjectiles) + { + var comp = EnsureComp(proj); + comp.Damage += component.Damage; + + if (!component.AddEmbedding) + continue; + + comp.AddEmbedding = true; + var embed = EnsureComp(proj); + embed.Offset = component.Offset; + embed.PreventEmbedding = false; + Dirty(proj, embed); + } + } +} diff --git a/Content.Server/White/Crossbow/PoweredComponent.cs b/Content.Server/White/Crossbow/PoweredComponent.cs new file mode 100644 index 0000000000..4ef3fef4ad --- /dev/null +++ b/Content.Server/White/Crossbow/PoweredComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Damage; + +namespace Content.Server.White.Crossbow; + +[RegisterComponent] +public sealed partial class PoweredComponent : Component +{ + [DataField("charge", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public float Charge; + + [DataField("damage", required: true)] + [ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier Damage = new(); +} diff --git a/Content.Server/White/Crossbow/PoweredSystem.cs b/Content.Server/White/Crossbow/PoweredSystem.cs new file mode 100644 index 0000000000..e349354e7c --- /dev/null +++ b/Content.Server/White/Crossbow/PoweredSystem.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Shared.Projectiles; +using Content.Shared.Weapons.Ranged.Events; +using Robust.Shared.Containers; +using Robust.Shared.Utility; + +namespace Content.Server.White.Crossbow; + +public sealed class PoweredSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly BatterySystem _battery = default!; + + private const string CellSlot = "cell_slot"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnShoot); + } + + private void OnShoot(EntityUid uid, PoweredComponent component, AmmoShotEvent args) + { + if (!TryGetBatteryComponent(uid, out var battery, out var batteryUid)) + return; + + var (factor, charge) = GetFactor(component, battery, batteryUid.Value); + + // Чтобы затриггерить взрыв на полную мощь, если есть плазма в батарейке + _battery.SetCharge(batteryUid.Value, battery.CurrentCharge, battery); + + _battery.SetCharge(batteryUid.Value, battery.CurrentCharge - charge, battery); + var damage = component.Damage * factor; + + foreach (var proj in args.FiredProjectiles) + { + EnsureComp(proj).Damage += damage; + + if (factor == 0) + continue; + + var embed = EnsureComp(proj); + embed.Penetrate = true; + Dirty(proj, embed); + } + } + + public float GetPowerCoefficient(EntityUid uid) + { + if (!TryComp(uid, out PoweredComponent? component) || + !TryGetBatteryComponent(uid, out var battery, out var batteryUid)) + return 1f; + + return 1f + GetFactor(component, battery, batteryUid.Value).Item1; + } + + private (float, float) GetFactor(PoweredComponent component, BatteryComponent battery, EntityUid batteryUid) + { + DebugTools.Assert(component.Charge != 0f); + var charge = MathF.Min(battery.CurrentCharge, component.Charge); + var factor = charge / component.Charge; + + if (TryComp(batteryUid, out RiggableComponent? rig) && rig.IsRigged) + factor *= 2f; + + return (factor, charge); + } + + private bool TryGetBatteryComponent(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, + [NotNullWhen(true)] out EntityUid? batteryUid) + { + if (!_containers.TryGetContainer(uid, CellSlot, out var container) || + container is not ContainerSlot slot) + { + battery = null; + batteryUid = null; + return false; + } + + batteryUid = slot.ContainedEntity; + + if (batteryUid != null) + return TryComp(batteryUid, out battery); + + battery = null; + return false; + } +} diff --git a/Content.Server/White/Crossbow/ThrowDamageModifierComponent.cs b/Content.Server/White/Crossbow/ThrowDamageModifierComponent.cs new file mode 100644 index 0000000000..48641d4e44 --- /dev/null +++ b/Content.Server/White/Crossbow/ThrowDamageModifierComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Damage; + +namespace Content.Server.White.Crossbow; + +[RegisterComponent] +public sealed partial class ThrowDamageModifierComponent : Component +{ + public DamageSpecifier Damage = new(); + + public bool AddEmbedding; + + public bool ClearDamageOnRemove; +} diff --git a/Content.Server/White/Crossbow/ThrowDamageModifierSystem.cs b/Content.Server/White/Crossbow/ThrowDamageModifierSystem.cs new file mode 100644 index 0000000000..24abf95286 --- /dev/null +++ b/Content.Server/White/Crossbow/ThrowDamageModifierSystem.cs @@ -0,0 +1,43 @@ +using Content.Shared.Projectiles; +using Content.Shared.Throwing; +using Content.Shared.White.Crossbow; + +namespace Content.Server.White.Crossbow; + +public sealed class ThrowDamageModifierSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStopped); + SubscribeLocalEvent(OnEmbedStart); + SubscribeLocalEvent(OnEmbedRemoved); + } + + private void OnEmbedStart(EntityUid uid, ThrowDamageModifierComponent component, ref EmbedStartEvent args) + { + component.ClearDamageOnRemove = true; + + if (component.AddEmbedding) + args.Embed.PreventEmbedding = true; + } + + private void OnEmbedRemoved(EntityUid uid, ThrowDamageModifierComponent component, EmbedRemovedEvent args) + { + if (!component.ClearDamageOnRemove) + return; + + component.ClearDamageOnRemove = false; + component.Damage.DamageDict.Clear(); + + if (component.AddEmbedding) + RemComp(uid); + } + + private void OnStopped(EntityUid uid, ThrowDamageModifierComponent component, StopThrowEvent args) + { + if (!component.ClearDamageOnRemove) + component.Damage.DamageDict.Clear(); + } +} diff --git a/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs index 008b7c2ced..0ea90bcbd7 100644 --- a/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs +++ b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs @@ -46,4 +46,15 @@ public sealed partial class EmbeddableProjectileComponent : Component /// [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] public SoundSpecifier? Sound; + + // WD START + [AutoNetworkedField] + public bool PreventEmbedding; + + [AutoNetworkedField] + public bool Penetrate; + + [AutoNetworkedField] + public EntityUid? PenetratedUid; + // WD END } diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index e003764f92..56ffc3c597 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.DoAfter; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Throwing; +using Content.Shared.White.Crossbow; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Network; @@ -26,6 +27,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly PenetratedSystem _penetratedSystem = default!; // WD public override void Initialize() { @@ -37,24 +39,19 @@ public abstract partial class SharedProjectileSystem : EntitySystem SubscribeLocalEvent(OnEmbedActivate); SubscribeLocalEvent(OnEmbedRemove); SubscribeLocalEvent(OnAttemptPacifiedThrow); + SubscribeLocalEvent(OnLand); // WD } private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args) { - // Nuh uh - if (component.RemovalTime == null) - return; - + // WD EDIT START if (args.Handled || !TryComp(uid, out var physics) || physics.BodyType != BodyType.Static) return; args.Handled = true; - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.RemovalTime.Value, - new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid) - { - DistanceThreshold = SharedInteractionSystem.InteractionRange, - }); + AttemptEmbedRemove(uid, args.User, component); + // WD EDIT END } private void OnEmbedRemove(EntityUid uid, EmbeddableProjectileComponent component, RemoveEmbeddedProjectileEvent args) @@ -82,6 +79,16 @@ public abstract partial class SharedProjectileSystem : EntitySystem projectile.DamagedEntity = false; } + // WD START + if (component.PenetratedUid != null) + { + _penetratedSystem.FreePenetrated(component.PenetratedUid.Value); + component.PenetratedUid = null; + } + + RaiseLocalEvent(uid, new EmbedRemovedEvent()); + // WD END + // Land it just coz uhhh yeah var landEv = new LandEvent(args.User, true); RaiseLocalEvent(uid, ref landEv); @@ -96,6 +103,28 @@ public abstract partial class SharedProjectileSystem : EntitySystem if (!component.EmbedOnThrow) return; + // WD START + if (component is {Penetrate: true, PenetratedUid: null} && + TryComp(args.Target, out PenetratedComponent? penetrated) && + penetrated is {ProjectileUid: null, IsPinned: false} && + TryComp(args.Target, out PhysicsComponent? physics)) + { + component.PenetratedUid = args.Target; + penetrated.ProjectileUid = uid; + _physics.SetLinearVelocity(args.Target, Vector2.Zero, body: physics); + _physics.SetBodyType(args.Target, BodyType.Static, body: physics); + var xform = Transform(args.Target); + _transform.SetParent(args.Target, xform, uid); + _transform.SetLocalPosition(args.Target, + xform.LocalPosition + Transform(uid).LocalRotation.RotateVec(new Vector2(0.5f, 0.5f)), xform); + Dirty(uid, component); + return; + } + + if (component.PenetratedUid == args.Target) + args.Handled = true; + // WD END + Embed(uid, args.Target, component); } @@ -113,6 +142,16 @@ public abstract partial class SharedProjectileSystem : EntitySystem private void Embed(EntityUid uid, EntityUid target, EmbeddableProjectileComponent component) { + if (component.PreventEmbedding || component.PenetratedUid == target) // WD START + return; + + var ev = new EmbedStartEvent(component); + RaiseLocalEvent(uid, ref ev); + + if (TryComp(component.PenetratedUid, out PenetratedComponent? penetrated)) + penetrated.IsPinned = true; + // WD END + TryComp(uid, out var physics); _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); _physics.SetBodyType(uid, BodyType.Static, body: physics); @@ -160,6 +199,37 @@ public abstract partial class SharedProjectileSystem : EntitySystem { args.Cancel("pacified-cannot-throw-embed"); } + + // WD EDIT START + private void OnLand(EntityUid uid, EmbeddableProjectileComponent component, ref LandEvent args) + { + if (component.PenetratedUid == null) + return; + + var penetratedUid = component.PenetratedUid.Value; + component.PenetratedUid = null; + + _penetratedSystem.FreePenetrated(penetratedUid); + + Embed(uid, penetratedUid, component); + } + + public void AttemptEmbedRemove(EntityUid uid, EntityUid user, EmbeddableProjectileComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + + // Nuh uh + if (component.RemovalTime == null) + return; + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.RemovalTime.Value, + new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid) + { + DistanceThreshold = SharedInteractionSystem.InteractionRange, + }); + } + // WD EDIT END } [Serializable, NetSerializable] diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index dc5be423a9..fa170a4637 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -167,6 +167,16 @@ public partial class GunComponent : Component /// [DataField("clumsyProof"), ViewVariables(VVAccess.ReadWrite)] public bool ClumsyProof = false; + + // WD START + [DataField("forceThrowingAngle")] + [ViewVariables(VVAccess.ReadWrite)] + public bool ForceThrowingAngle; + + [DataField("angle")] + [ViewVariables(VVAccess.ReadWrite)] + public Angle Angle; + // WD END } [Flags] diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index 501d0069a7..cd0d768098 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -2,6 +2,7 @@ using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; +using Content.Shared.Stacks; using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; @@ -47,8 +48,17 @@ public abstract partial class SharedGunSystem if (GetBallisticShots(component) >= component.Capacity) return; - component.Entities.Add(args.Used); - Containers.Insert(args.Used, component.Container); + var entity = args.Used; // WD EDIT START + var doInsert = true; + if (TryComp(args.Used, out StackComponent? stack) && stack.Count > 1) + { + entity = GetStackEntity(args.Used, stack); + doInsert = false; + } + + component.Entities.Add(entity); + if (_netManager.IsServer || doInsert) + Containers.Insert(entity, component.Container); // WD EDIT END // Not predicted so Audio.PlayPredicted(component.SoundInsert, uid, args.User); args.Handled = true; @@ -56,6 +66,8 @@ public abstract partial class SharedGunSystem Dirty(uid, component); } + protected virtual EntityUid GetStackEntity(EntityUid uid, StackComponent stack) { return uid; } + private void OnBallisticAfterInteract(EntityUid uid, BallisticAmmoProviderComponent component, AfterInteractEvent args) { if (args.Handled || diff --git a/Content.Shared/White/Crossbow/DrawableComponent.cs b/Content.Shared/White/Crossbow/DrawableComponent.cs new file mode 100644 index 0000000000..fb41861125 --- /dev/null +++ b/Content.Shared/White/Crossbow/DrawableComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Weapons.Ranged.Components; +using Robust.Shared.Audio; + +namespace Content.Shared.White.Crossbow; + +[RegisterComponent] +public sealed partial class DrawableComponent : Component +{ + [ViewVariables] + public bool Drawn; + + public BallisticAmmoProviderComponent Provider = default!; + + [ViewVariables(VVAccess.ReadWrite), DataField("soundInsert")] + public SoundSpecifier? SoundDraw = new SoundPathSpecifier("/Audio/Weapons/drawbow2.ogg"); +} diff --git a/Content.Shared/White/Crossbow/DrawableSystem.cs b/Content.Shared/White/Crossbow/DrawableSystem.cs new file mode 100644 index 0000000000..72340f43ec --- /dev/null +++ b/Content.Shared/White/Crossbow/DrawableSystem.cs @@ -0,0 +1,71 @@ +using Content.Shared.Interaction.Events; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Serialization; + +namespace Content.Shared.White.Crossbow; + +public sealed class DrawableSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnItemRemove); + SubscribeLocalEvent(OnAttemptShoot); + SubscribeLocalEvent(OnUse); + } + + private void OnUse(EntityUid uid, DrawableComponent component, UseInHandEvent args) + { + if (component.Drawn || component.Provider.Count == 0) + return; + + args.Handled = true; + + _audio.PlayPredicted(component.SoundDraw, uid, args.User); + component.Drawn = true; + + UpdateDrawableAppearance(uid, component); + } + + private void OnItemRemove(EntityUid uid, DrawableComponent component, EntRemovedFromContainerMessage args) + { + if (!component.Drawn || args.Container.ID != component.Provider.Container.ID) + return; + + component.Drawn = false; + UpdateDrawableAppearance(uid, component); + } + + private void OnStartup(EntityUid uid, DrawableComponent component, ComponentStartup args) + { + component.Provider = EnsureComp(uid); + } + + private void OnAttemptShoot(EntityUid uid, DrawableComponent component, ref AttemptShootEvent args) + { + if (!component.Drawn) + args.Cancelled = true; + } + + private void UpdateDrawableAppearance(EntityUid uid, DrawableComponent component) + { + if (!TryComp(uid, out var appearance)) + return; + + _appearance.SetData(uid, DrawableVisuals.Drawn, component.Drawn, appearance); + } +} + +[Serializable, NetSerializable] +public enum DrawableVisuals : byte +{ + Drawn +} diff --git a/Content.Shared/White/Crossbow/EmbedEvents.cs b/Content.Shared/White/Crossbow/EmbedEvents.cs new file mode 100644 index 0000000000..25c05d34df --- /dev/null +++ b/Content.Shared/White/Crossbow/EmbedEvents.cs @@ -0,0 +1,10 @@ +using Content.Shared.Projectiles; + +namespace Content.Shared.White.Crossbow; + +[ByRefEvent] +public readonly record struct EmbedStartEvent(EmbeddableProjectileComponent Embed); + +public sealed class EmbedRemovedEvent : EntityEventArgs +{ +} diff --git a/Content.Shared/White/Crossbow/PenetratedComponent.cs b/Content.Shared/White/Crossbow/PenetratedComponent.cs new file mode 100644 index 0000000000..334bbc0992 --- /dev/null +++ b/Content.Shared/White/Crossbow/PenetratedComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.White.Crossbow; + +[RegisterComponent] +public sealed partial class PenetratedComponent : Component +{ + public EntityUid? ProjectileUid; + + public bool IsPinned; +} diff --git a/Content.Shared/White/Crossbow/PenetratedSystem.cs b/Content.Shared/White/Crossbow/PenetratedSystem.cs new file mode 100644 index 0000000000..ad679fe122 --- /dev/null +++ b/Content.Shared/White/Crossbow/PenetratedSystem.cs @@ -0,0 +1,41 @@ +using Content.Shared.Movement.Events; +using Content.Shared.Projectiles; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; + +namespace Content.Shared.White.Crossbow; + +public sealed class PenetratedSystem : EntitySystem +{ + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedProjectileSystem _projectile = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMoveInput); + } + + private void OnMoveInput(EntityUid uid, PenetratedComponent component, ref MoveInputEvent args) + { + if (component is {ProjectileUid: not null, IsPinned: true}) + _projectile.AttemptEmbedRemove(component.ProjectileUid.Value, uid); + } + + public void FreePenetrated(EntityUid uid, PenetratedComponent? penetrated = null) + { + if (!Resolve(uid, ref penetrated, false)) + return; + + var xform = Transform(uid); + TryComp(uid, out var physics); + _physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform); + _transform.AttachToGridOrMap(uid, xform); + penetrated.ProjectileUid = null; + penetrated.IsPinned = false; + _physics.WakeBody(uid, body: physics); + } +} diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 2dffc838ef..9652da27d4 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -233,6 +233,7 @@ - type: DeathGasps - type: ExaminableClothes - type: CharacterInformation + - type: Penetrated - type: entity save: false diff --git a/Resources/Prototypes/Entities/Objects/Materials/parts.yml b/Resources/Prototypes/Entities/Objects/Materials/parts.yml index a15675995c..4db618761e 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/parts.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/parts.yml @@ -32,6 +32,10 @@ - type: PhysicalComposition materialComposition: Steel: 50 #Half of a regular steel sheet to reflect the crafting recipe + - type: Tag + tags: + - DroneUsable + - CrossbowBolt - type: Stack stackType: MetalRod baseLayer: base @@ -82,6 +86,7 @@ tags: - RodMetal1 - DroneUsable + - CrossbowBolt - type: Sprite state: rods - type: Stack @@ -97,10 +102,31 @@ tags: - RodMetal1 - DroneUsable + - CrossbowBolt + - type: Ammo + muzzleFlash: null + - type: DamageOtherOnHit + damage: + types: + Blunt: 0 - type: Sprite state: rods - type: Stack count: 1 + - type: Fixtures + fixtures: + fix1: + shape: !type:PolygonShape + vertices: + - -0.20,-0.10 + - -0.10,-0.20 + - 0.40,0.30 + - 0.30,0.40 + density: 20 + mask: + - ItemMask + restitution: 0.3 + friction: 0.2 - type: entity parent: PartRodMetal diff --git a/Resources/Prototypes/Entities/Objects/Misc/improvised_gun_parts.yml b/Resources/Prototypes/Entities/Objects/Misc/improvised_gun_parts.yml index abf4e0974a..bc7be35905 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/improvised_gun_parts.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/improvised_gun_parts.yml @@ -28,8 +28,9 @@ sprite: Objects/Misc/rifle_stock.rsi state: icon - type: Construction - graph: RifleStockGraph - node: riflestock + deconstructionTarget: null + graph: WeaponPoweredCrossbowGraph + node: stock - type: Tag tags: - RifleStock diff --git a/Resources/Prototypes/White/Entities/Objects/Weapons/Guns/crossbow.yml b/Resources/Prototypes/White/Entities/Objects/Weapons/Guns/crossbow.yml new file mode 100644 index 0000000000..25d361c28e --- /dev/null +++ b/Resources/Prototypes/White/Entities/Objects/Weapons/Guns/crossbow.yml @@ -0,0 +1,79 @@ +- type: entity + name: арбалет + parent: BaseItem + id: WeaponPoweredCrossbow + description: Опасная штука, страшная вещь. + components: + - type: Sprite + sprite: White/Objects/Weapons/crossbow.rsi + state: base + - type: Clothing + quickEquip: false + slots: + - Back + - type: Item + size: 80 + sprite: White/Objects/Weapons/crossbow.rsi + - type: Gun + forceThrowingAngle: true + angle: 225 + projectileSpeed: 35 + fireRate: 0.5 + soundGunshot: + path: /Audio/Weapons/click.ogg + - type: BallisticAmmoProvider + whitelist: + tags: + - CrossbowBolt + capacity: 1 + soundInsert: + path: /Audio/Weapons/Guns/MagIn/revolver_magin.ogg + - type: ContainerContainer + containers: + ballistic-ammo: !type:Container + ents: [] + cell_slot: !type:ContainerSlot + - type: PowerCellSlot + cellSlotId: cell_slot + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + - type: Drawable + - type: Appearance + - type: ModifyDamageOnShoot + addEmbedding: true + offset: 0.2,0.2 + damage: + types: + Blunt: 15 + - type: Powered + charge: 360 + damage: + types: + Blunt: 30 + - type: Construction + deconstructionTarget: null + graph: WeaponPoweredCrossbowGraph + node: crossbow + +- type: entity + name: часть арбалета + parent: BaseItem + id: WeaponPoweredCrossbowUnfinished + description: Недоделанный арбалет. + components: + - type: Sprite + sprite: White/Objects/Weapons/crossbow.rsi + state: base + - type: Item + size: 80 + sprite: White/Objects/Weapons/crossbow.rsi + - type: Clothing + quickEquip: false + slots: + - Back + - type: Construction + deconstructionTarget: null + graph: WeaponPoweredCrossbowGraph + node: unfinished diff --git a/Resources/Prototypes/White/Recipes/hidden_crafts.yml b/Resources/Prototypes/White/Recipes/hidden_crafts.yml index f5df7fae0f..c3afbfe4fb 100644 --- a/Resources/Prototypes/White/Recipes/hidden_crafts.yml +++ b/Resources/Prototypes/White/Recipes/hidden_crafts.yml @@ -43,3 +43,55 @@ doAfter: 1 - node: flamethrower entity: WeaponFlamethrower + +- type: constructionGraph + id: WeaponPoweredCrossbowGraph + start: stock + graph: + - node: stock + entity: RifleStock + edges: + - to: unfinished + steps: + - material: MetalRod + amount: 3 + doAfter: 3 + - node: unfinished + edges: + - to: rods + - node: unfinished + entity: WeaponPoweredCrossbowUnfinished + edges: + - to: welded + steps: + - tool: Welding + doAfter: 5 + - node: welded + edges: + - to: cables + steps: + - material: Cable + amount: 5 + doAfter: 0.5 + - node: cables + edges: + - to: plastic + steps: + - material: Plastic + amount: 3 + doAfter: 0.5 + - node: plastic + edges: + - to: unscrewed + steps: + - material: Cable + amount: 5 + doAfter: 0.5 + - node: unscrewed + edges: + - to: crossbow + steps: + - tool: Screwing + doAfter: 1 + - node: crossbow + entity: WeaponPoweredCrossbow diff --git a/Resources/Prototypes/White/tags.yml b/Resources/Prototypes/White/tags.yml index 77fb65e2eb..776329923e 100644 --- a/Resources/Prototypes/White/tags.yml +++ b/Resources/Prototypes/White/tags.yml @@ -3,3 +3,6 @@ - type: Tag id: EnergySword + +- type: Tag + id: CrossbowBolt diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/base.png b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/base.png new file mode 100644 index 0000000000..62741449a4 Binary files /dev/null and b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/base.png differ diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/drawn.png b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/drawn.png new file mode 100644 index 0000000000..480133d9c6 Binary files /dev/null and b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/drawn.png differ diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-left.png b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-left.png new file mode 100644 index 0000000000..385c96238d Binary files /dev/null and b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-left.png differ diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-right.png b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-right.png new file mode 100644 index 0000000000..93a062d9ee Binary files /dev/null and b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/inhand-right.png differ diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/loaded.png b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/loaded.png new file mode 100644 index 0000000000..36cb5cf2e7 Binary files /dev/null and b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/loaded.png differ diff --git a/Resources/Textures/White/Objects/Weapons/crossbow.rsi/meta.json b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/meta.json new file mode 100644 index 0000000000..fa3ce2ddec --- /dev/null +++ b/Resources/Textures/White/Objects/Weapons/crossbow.rsi/meta.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from paradise at https://github.com/ParadiseSS13/Paradise at 76d0428022d17f3249585d96ac9b69076206efd4", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "loaded" + }, + { + "name": "drawn" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +}