diff --git a/Content.Server/Chemistry/Components/SolutionInjectWhileEmbeddedComponent.cs b/Content.Server/Chemistry/Components/SolutionInjectWhileEmbeddedComponent.cs
new file mode 100644
index 0000000000..0f10e2a449
--- /dev/null
+++ b/Content.Server/Chemistry/Components/SolutionInjectWhileEmbeddedComponent.cs
@@ -0,0 +1,24 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+
+namespace Content.Server.Chemistry.Components;
+
+///
+/// Used for embeddable entities that should try to inject a
+/// contained solution into a target over time while they are embbeded into.
+///
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
+ ///
+ ///The time at which the injection will happen.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan NextUpdate;
+
+ ///
+ ///The delay between each injection in seconds.
+ ///
+ [DataField]
+ public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
+}
+
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
index d56fded024..f15edcf067 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
SubscribeLocalEvent(HandleProjectileHit);
SubscribeLocalEvent(HandleEmbed);
SubscribeLocalEvent(HandleMeleeHit);
+ SubscribeLocalEvent(OnInjectOverTime);
}
private void HandleProjectileHit(Entity entity, ref ProjectileHitEvent args)
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
}
+ private void OnInjectOverTime(Entity entity, ref InjectOverTimeEvent args)
+ {
+ DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
+ }
+
private void DoInjection(Entity injectorEntity, EntityUid target, EntityUid? source = null)
{
TryInjectTargets(injectorEntity, [target], source);
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectWhileEmbeddedSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectWhileEmbeddedSystem.cs
new file mode 100644
index 0000000000..2baeba9da1
--- /dev/null
+++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectWhileEmbeddedSystem.cs
@@ -0,0 +1,60 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Events;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Tag;
+using Content.Shared.Weapons.Melee.Events;
+using Robust.Shared.Collections;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Chemistry.EntitySystems;
+
+///
+/// System for handling injecting into an entity while a projectile is embedded.
+///
+public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly TagSystem _tag = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var injectComponent, out var projectileComponent))
+ {
+ if (_gameTiming.CurTime < injectComponent.NextUpdate)
+ continue;
+
+ injectComponent.NextUpdate += injectComponent.UpdateInterval;
+
+ if(projectileComponent.EmbeddedIntoUid == null)
+ continue;
+
+ var ev = new InjectOverTimeEvent(projectileComponent.EmbeddedIntoUid.Value);
+ RaiseLocalEvent(uid, ref ev);
+
+ }
+ }
+}
diff --git a/Content.Shared/Chemistry/InjectOverTimeEvent.cs b/Content.Shared/Chemistry/InjectOverTimeEvent.cs
new file mode 100644
index 0000000000..ca5ab4213f
--- /dev/null
+++ b/Content.Shared/Chemistry/InjectOverTimeEvent.cs
@@ -0,0 +1,13 @@
+namespace Content.Shared.Chemistry.Events;
+
+///
+/// Raised directed on an entity when it embeds in another entity.
+///
+[ByRefEvent]
+public readonly record struct InjectOverTimeEvent(EntityUid embeddedIntoUid)
+{
+ ///
+ /// Entity that is embedded in.
+ ///
+ public readonly EntityUid EmbeddedIntoUid = embeddedIntoUid;
+}
diff --git a/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs
index 008b7c2ced..e4125945d2 100644
--- a/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs
+++ b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs
@@ -13,37 +13,43 @@ public sealed partial class EmbeddableProjectileComponent : Component
///
/// Minimum speed of the projectile to embed.
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public float MinimumSpeed = 5f;
///
/// Delete the entity on embedded removal?
/// Does nothing if there's no RemovalTime.
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public bool DeleteOnRemove;
///
/// How long it takes to remove the embedded object.
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public float? RemovalTime = 3f;
///
/// Whether this entity will embed when thrown, or only when shot as a projectile.
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public bool EmbedOnThrow = true;
///
/// How far into the entity should we offset (0 is wherever we collided).
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public Vector2 Offset = Vector2.Zero;
///
/// Sound to play after embedding into a hit target.
///
- [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public SoundSpecifier? Sound;
+
+ ///
+ /// Uid of the entity the projectile is embed into.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? EmbeddedIntoUid;
}
diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs
index 1b7d6d6f99..85e75d6d29 100644
--- a/Content.Shared/Projectiles/SharedProjectileSystem.cs
+++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs
@@ -71,6 +71,8 @@ public abstract partial class SharedProjectileSystem : EntitySystem
TryComp(uid, out var physics);
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
_transform.AttachToGridOrMap(uid, xform);
+ component.EmbeddedIntoUid = null;
+ Dirty(uid, component);
// Reset whether the projectile has damaged anything if it successfully was removed
if (TryComp(uid, out var projectile))
@@ -127,8 +129,10 @@ public abstract partial class SharedProjectileSystem : EntitySystem
}
_audio.PlayPredicted(component.Sound, uid, null);
+ component.EmbeddedIntoUid = target;
var ev = new EmbedEvent(user, target);
RaiseLocalEvent(uid, ref ev);
+ Dirty(uid, component);
}
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
diff --git a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml
index 7c46974818..89db045c96 100644
--- a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml
+++ b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml
@@ -26,4 +26,9 @@
- files: ["ship_duster.ogg", "ship_friendship.ogg", "ship_svalinn.ogg", "ship_perforator.ogg"]
license: "CC0-1.0"
copyright: "Created by MIXnikita for Space Station 14"
- source: "https://github.com/MIXnikita"
\ No newline at end of file
+ source: "https://github.com/MIXnikita"
+
+- files: ["syringe_gun.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from vgstation"
+ source: "https://github.com/vgstation-coders/vgstation13/commit/23303188abe6fe31b114a218a1950d7325a23730"
\ No newline at end of file
diff --git a/Resources/Audio/Weapons/Guns/Gunshots/syringe_gun.ogg b/Resources/Audio/Weapons/Guns/Gunshots/syringe_gun.ogg
new file mode 100644
index 0000000000..2132808ecd
Binary files /dev/null and b/Resources/Audio/Weapons/Guns/Gunshots/syringe_gun.ogg differ
diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
index 0f74b4afaf..3f2ac403ac 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
@@ -386,6 +386,66 @@
- Syringe
- Trash
+- type: entity
+ name: mini syringe
+ parent: Syringe
+ description: A regular syringe, reshaped to fit inside of a gun.
+ id: MiniSyringe
+ components:
+ - type: Sprite
+ sprite: Objects/Specific/Chemistry/syringe.rsi
+ layers:
+ - state: minisyringe1
+ map: ["enum.SolutionContainerLayers.Fill"]
+ visible: false
+ - state: syringeproj
+ - type: SolutionContainerVisuals
+ maxFillLevels: 3
+ fillBaseName: minisyringe
+ inHandsMaxFillLevels: 3
+ inHandsFillBaseName: -fill-
+ - type: EmbeddableProjectile
+ offset: "-0.1,0"
+ minimumSpeed: 3
+ removalTime: 0.25
+ embedOnThrow: false
+ - type: SolutionInjectWhileEmbedded
+ transferAmount: 1
+ solution: injector
+ updateInterval: 2
+ - type: SolutionInjectOnEmbed
+ transferAmount: 2
+ solution: injector
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.2
+ density: 5
+ mask:
+ - ItemMask
+ restitution: 0.3
+ friction: 0.2
+ projectile:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.1,-0.3,0.1,0.3"
+ hard: false
+ mask:
+ - Impassable
+ - BulletImpassable
+ - type: Projectile
+ deleteOnCollide: false
+ onlyCollideWhenShot: true
+ damage:
+ types:
+ Piercing: 5
+ - type: Tag
+ tags:
+ - Syringe
+ - Trash
+ - SyringeGunAmmo
+
- type: entity
parent: BaseSyringe
id: PrefilledSyringe
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml
index 1d18c2b050..63a7acd257 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/pneumatic_cannon.yml
@@ -103,6 +103,43 @@
containers:
storagebase: !type:Container
ents: []
+
+- type: entity
+ name: syringe gun
+ parent: BaseStorageItem
+ id: LauncherSyringe
+ description: Load full of poisoned syringes for optimal fun.
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Cannons/syringe_gun.rsi
+ layers:
+ - state: syringe_gun
+ - type: Storage
+ maxItemSize: Normal
+ grid:
+ - 0,0,2,0
+ whitelist:
+ tags:
+ - SyringeGunAmmo
+ - type: Gun
+ fireRate: 1
+ selectedMode: SemiAuto
+ availableModes:
+ - SemiAuto
+ - FullAuto
+ soundGunshot:
+ path: /Audio/Weapons/Guns/Gunshots/syringe_gun.ogg
+ soundEmpty:
+ path: /Audio/Weapons/Guns/Empty/empty.ogg
+ clumsyProof: true
+ - type: ContainerAmmoProvider
+ container: storagebase
+ - type: Item
+ size: Normal
+ - type: ContainerContainer
+ containers:
+ storagebase: !type:Container
+ ents: []
# shoots bullets instead of throwing them, no other changes
- type: entity
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index d3ffcdcac1..8962da5790 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -1300,6 +1300,9 @@
- type: Tag
id: Syringe
+- type: Tag
+ id: SyringeGunAmmo
+
- type: Tag
id: Spellbook
diff --git a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/meta.json b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/meta.json
index 1495eccd7a..0c29f3e3fb 100644
--- a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/meta.json
+++ b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/meta.json
@@ -57,6 +57,15 @@
{
"name": "syringe2"
},
+ {
+ "name": "minisyringe1"
+ },
+ {
+ "name": "minisyringe2"
+ },
+ {
+ "name": "minisyringe3"
+ },
{
"name": "inhand-left",
"directions": 4
diff --git a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe1.png b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe1.png
new file mode 100644
index 0000000000..66c49a744c
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe1.png differ
diff --git a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe2.png b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe2.png
new file mode 100644
index 0000000000..6f5d566354
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe2.png differ
diff --git a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe3.png b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe3.png
new file mode 100644
index 0000000000..71d3bad044
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/minisyringe3.png differ
diff --git a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/syringeproj.png b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/syringeproj.png
index 7819a48c66..5faf746ae3 100644
Binary files a/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/syringeproj.png and b/Resources/Textures/Objects/Specific/Chemistry/syringe.rsi/syringeproj.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-left.png
new file mode 100644
index 0000000000..b59a4d5268
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-right.png
new file mode 100644
index 0000000000..6a8268d27e
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/meta.json
new file mode 100644
index 0000000000..dae584eb81
--- /dev/null
+++ b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/meta.json
@@ -0,0 +1,22 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from vgstation13 at https://github.com/vgstation-coders/vgstation13 at f91dfe2e0dba1b7a8b9a0fa18251340920979a62, held sprite by ScarKy0",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "syringe_gun"
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/syringe_gun.png b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/syringe_gun.png
new file mode 100644
index 0000000000..972714d1c7
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Cannons/syringe_gun.rsi/syringe_gun.png differ