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
This commit is contained in:
ScarKy0
2024-10-22 15:03:42 +02:00
committed by GitHub
parent 94bbf7262c
commit 62f5a31c4a
20 changed files with 257 additions and 7 deletions

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View 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;
}

View File

@@ -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;
} }

View File

@@ -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)

View File

@@ -26,4 +26,9 @@
- files: ["ship_duster.ogg", "ship_friendship.ogg", "ship_svalinn.ogg", "ship_perforator.ogg"] - files: ["ship_duster.ogg", "ship_friendship.ogg", "ship_svalinn.ogg", "ship_perforator.ogg"]
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"

Binary file not shown.

View 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

View File

@@ -103,6 +103,43 @@
containers: containers:
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

View File

@@ -1300,6 +1300,9 @@
- type: Tag - type: Tag
id: Syringe id: Syringe
- type: Tag
id: SyringeGunAmmo
- type: Tag - type: Tag
id: Spellbook id: Spellbook

View File

@@ -57,6 +57,15 @@
{ {
"name": "syringe2" "name": "syringe2"
}, },
{
"name": "minisyringe1"
},
{
"name": "minisyringe2"
},
{
"name": "minisyringe3"
},
{ {
"name": "inhand-left", "name": "inhand-left",
"directions": 4 "directions": 4

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -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
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B