Syringe gun! (#32112)
* Init testing * copyright * oops * Tracking the embed entity uid * testing stuff for gradual injection * work * weh * god save me * bleh * Yippee! * Again * Mini syringe ammo * cleaning up * mini syringes have a texture for fill amount * -3 cool points :( * hitboxes * init cleanup * much needed fixes * Fixes
@@ -0,0 +1,24 @@
|
|||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Content.Server.Chemistry.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for embeddable entities that should try to inject a
|
||||||
|
/// contained solution into a target over time while they are embbeded into.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, AutoGenerateComponentPause]
|
||||||
|
public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
|
||||||
|
///<summary>
|
||||||
|
///The time at which the injection will happen.
|
||||||
|
///</summary>
|
||||||
|
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||||
|
public TimeSpan NextUpdate;
|
||||||
|
|
||||||
|
///<summary>
|
||||||
|
///The delay between each injection in seconds.
|
||||||
|
///</summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
|
|||||||
using Content.Server.Body.Systems;
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.Chemistry.Components;
|
using Content.Server.Chemistry.Components;
|
||||||
using Content.Shared.Chemistry.EntitySystems;
|
using Content.Shared.Chemistry.EntitySystems;
|
||||||
|
using Content.Shared.Chemistry.Events;
|
||||||
using Content.Shared.Inventory;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Projectiles;
|
using Content.Shared.Projectiles;
|
||||||
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
|||||||
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
||||||
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
||||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
||||||
|
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
|
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
|
||||||
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
|||||||
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
|
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnInjectOverTime(Entity<SolutionInjectWhileEmbeddedComponent> entity, ref InjectOverTimeEvent args)
|
||||||
|
{
|
||||||
|
DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
|
||||||
|
}
|
||||||
|
|
||||||
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
|
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
|
||||||
{
|
{
|
||||||
TryInjectTargets(injectorEntity, [target], source);
|
TryInjectTargets(injectorEntity, [target], source);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System for handling injecting into an entity while a projectile is embedded.
|
||||||
|
/// </summary>
|
||||||
|
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<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(Entity<SolutionInjectWhileEmbeddedComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
var query = EntityQueryEnumerator<SolutionInjectWhileEmbeddedComponent, EmbeddableProjectileComponent>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Content.Shared/Chemistry/InjectOverTimeEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Content.Shared.Chemistry.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised directed on an entity when it embeds in another entity.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct InjectOverTimeEvent(EntityUid embeddedIntoUid)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entity that is embedded in.
|
||||||
|
/// </summary>
|
||||||
|
public readonly EntityUid EmbeddedIntoUid = embeddedIntoUid;
|
||||||
|
}
|
||||||
@@ -13,37 +13,43 @@ public sealed partial class EmbeddableProjectileComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimum speed of the projectile to embed.
|
/// Minimum speed of the projectile to embed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public float MinimumSpeed = 5f;
|
public float MinimumSpeed = 5f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete the entity on embedded removal?
|
/// Delete the entity on embedded removal?
|
||||||
/// Does nothing if there's no RemovalTime.
|
/// Does nothing if there's no RemovalTime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public bool DeleteOnRemove;
|
public bool DeleteOnRemove;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How long it takes to remove the embedded object.
|
/// How long it takes to remove the embedded object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public float? RemovalTime = 3f;
|
public float? RemovalTime = 3f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this entity will embed when thrown, or only when shot as a projectile.
|
/// Whether this entity will embed when thrown, or only when shot as a projectile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public bool EmbedOnThrow = true;
|
public bool EmbedOnThrow = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How far into the entity should we offset (0 is wherever we collided).
|
/// How far into the entity should we offset (0 is wherever we collided).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public Vector2 Offset = Vector2.Zero;
|
public Vector2 Offset = Vector2.Zero;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sound to play after embedding into a hit target.
|
/// Sound to play after embedding into a hit target.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public SoundSpecifier? Sound;
|
public SoundSpecifier? Sound;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uid of the entity the projectile is embed into.
|
||||||
|
/// </summary>
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public EntityUid? EmbeddedIntoUid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
|||||||
TryComp<PhysicsComponent>(uid, out var physics);
|
TryComp<PhysicsComponent>(uid, out var physics);
|
||||||
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
|
_physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform);
|
||||||
_transform.AttachToGridOrMap(uid, xform);
|
_transform.AttachToGridOrMap(uid, xform);
|
||||||
|
component.EmbeddedIntoUid = null;
|
||||||
|
Dirty(uid, component);
|
||||||
|
|
||||||
// Reset whether the projectile has damaged anything if it successfully was removed
|
// Reset whether the projectile has damaged anything if it successfully was removed
|
||||||
if (TryComp<ProjectileComponent>(uid, out var projectile))
|
if (TryComp<ProjectileComponent>(uid, out var projectile))
|
||||||
@@ -127,8 +129,10 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
_audio.PlayPredicted(component.Sound, uid, null);
|
_audio.PlayPredicted(component.Sound, uid, null);
|
||||||
|
component.EmbeddedIntoUid = target;
|
||||||
var ev = new EmbedEvent(user, target);
|
var ev = new EmbedEvent(user, target);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
RaiseLocalEvent(uid, ref ev);
|
||||||
|
Dirty(uid, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
|
private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args)
|
||||||
|
|||||||
@@ -27,3 +27,8 @@
|
|||||||
license: "CC0-1.0"
|
license: "CC0-1.0"
|
||||||
copyright: "Created by MIXnikita for Space Station 14"
|
copyright: "Created by MIXnikita for Space Station 14"
|
||||||
source: "https://github.com/MIXnikita"
|
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"
|
||||||
BIN
Resources/Audio/Weapons/Guns/Gunshots/syringe_gun.ogg
Normal file
@@ -386,6 +386,66 @@
|
|||||||
- Syringe
|
- Syringe
|
||||||
- Trash
|
- 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
|
- type: entity
|
||||||
parent: BaseSyringe
|
parent: BaseSyringe
|
||||||
id: PrefilledSyringe
|
id: PrefilledSyringe
|
||||||
|
|||||||
@@ -104,6 +104,43 @@
|
|||||||
storagebase: !type:Container
|
storagebase: !type:Container
|
||||||
ents: []
|
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
|
# shoots bullets instead of throwing them, no other changes
|
||||||
- type: entity
|
- type: entity
|
||||||
parent: WeaponImprovisedPneumaticCannon
|
parent: WeaponImprovisedPneumaticCannon
|
||||||
|
|||||||
@@ -1300,6 +1300,9 @@
|
|||||||
- type: Tag
|
- type: Tag
|
||||||
id: Syringe
|
id: Syringe
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: SyringeGunAmmo
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: Spellbook
|
id: Spellbook
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,15 @@
|
|||||||
{
|
{
|
||||||
"name": "syringe2"
|
"name": "syringe2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "minisyringe1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minisyringe2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minisyringe3"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "inhand-left",
|
"name": "inhand-left",
|
||||||
"directions": 4
|
"directions": 4
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 341 B |
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 446 B |