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.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<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
||||
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
||||
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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>
|
||||
/// Minimum speed of the projectile to embed.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float MinimumSpeed = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Delete the entity on embedded removal?
|
||||
/// Does nothing if there's no RemovalTime.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool DeleteOnRemove;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to remove the embedded object.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float? RemovalTime = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity will embed when thrown, or only when shot as a projectile.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool EmbedOnThrow = true;
|
||||
|
||||
/// <summary>
|
||||
/// How far into the entity should we offset (0 is wherever we collided).
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public Vector2 Offset = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play after embedding into a hit target.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
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);
|
||||
_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<ProjectileComponent>(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)
|
||||
|
||||
@@ -27,3 +27,8 @@
|
||||
license: "CC0-1.0"
|
||||
copyright: "Created by MIXnikita for Space Station 14"
|
||||
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
|
||||
- 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
|
||||
|
||||
@@ -104,6 +104,43 @@
|
||||
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
|
||||
parent: WeaponImprovisedPneumaticCannon
|
||||
|
||||
@@ -1300,6 +1300,9 @@
|
||||
- type: Tag
|
||||
id: Syringe
|
||||
|
||||
- type: Tag
|
||||
id: SyringeGunAmmo
|
||||
|
||||
- type: Tag
|
||||
id: Spellbook
|
||||
|
||||
|
||||
@@ -57,6 +57,15 @@
|
||||
{
|
||||
"name": "syringe2"
|
||||
},
|
||||
{
|
||||
"name": "minisyringe1"
|
||||
},
|
||||
{
|
||||
"name": "minisyringe2"
|
||||
},
|
||||
{
|
||||
"name": "minisyringe3"
|
||||
},
|
||||
{
|
||||
"name": "inhand-left",
|
||||
"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 |