Implanters and Subdermal Implants (#11840)

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
keronshb
2022-11-20 01:49:37 -05:00
committed by GitHub
parent a5dff7eee7
commit 671324bef8
48 changed files with 1633 additions and 79 deletions

View File

@@ -0,0 +1,33 @@
using Content.Client.Implants.UI;
using Content.Client.Items;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.GameStates;
namespace Content.Client.Implants;
public sealed class ImplanterSystem : SharedImplanterSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ImplanterComponent, ComponentHandleState>(OnHandleImplanterState);
SubscribeLocalEvent<ImplanterComponent, ItemStatusCollectMessage>(OnItemImplanterStatus);
}
private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref ComponentHandleState args)
{
if (args.Current is not ImplanterComponentState state)
return;
component.CurrentMode = state.CurrentMode;
component.ImplantOnly = state.ImplantOnly;
component.UiUpdateNeeded = true;
}
private void OnItemImplanterStatus(EntityUid uid, ImplanterComponent component, ItemStatusCollectMessage args)
{
args.Controls.Add(new ImplanterStatusControl(component));
}
}

View File

@@ -0,0 +1,53 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Implants.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Implants.UI;
public sealed class ImplanterStatusControl : Control
{
private readonly ImplanterComponent _parent;
private readonly RichTextLabel _label;
public ImplanterStatusControl(ImplanterComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
Update();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_parent.UiUpdateNeeded)
return;
Update();
}
private void Update()
{
_parent.UiUpdateNeeded = false;
var modeStringLocalized = _parent.CurrentMode switch
{
ImplanterToggleMode.Draw => Loc.GetString("implanter-draw-text"),
ImplanterToggleMode.Inject => Loc.GetString("implanter-inject-text"),
_ => Loc.GetString("injector-invalid-injector-toggle-mode")
};
var entitiesStringLocalized = _parent.ImplanterSlot.HasItem switch
{
false => Loc.GetString("implanter-empty-text"),
true => Loc.GetString("implanter-implant-text", ("implantName", _parent.ImplantData.Item1), ("implantDescription", _parent.ImplantData.Item2), ("lineBreak", "\n")),
};
_label.SetMarkup(Loc.GetString("implanter-label", ("currentEntities", entitiesStringLocalized), ("modeString", modeStringLocalized), ("lineBreak", "\n")));
}
}

View File

@@ -130,7 +130,7 @@ public sealed class BodySystem : SharedBodySystem
InitPart(partComponent, prototype, prototype.Root);
}
public override HashSet<EntityUid> GibBody(EntityUid? bodyId, bool gibOrgans = false, BodyComponent? body = null)
public override HashSet<EntityUid> GibBody(EntityUid? bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false)
{
if (bodyId == null || !Resolve(bodyId.Value, ref body, false))
return new HashSet<EntityUid>();
@@ -150,9 +150,16 @@ public sealed class BodySystem : SharedBodySystem
{
foreach (var ent in cont.ContainedEntities)
{
cont.ForceRemove(ent);
Transform(ent).Coordinates = coordinates;
ent.RandomOffset(0.25f);
if (deleteItems)
{
QueueDel(ent);
}
else
{
cont.ForceRemove(ent);
Transform(ent).Coordinates = coordinates;
ent.RandomOffset(0.25f);
}
}
}
}

View File

@@ -36,7 +36,7 @@ namespace Content.Server.Cuffs.Components
[ViewVariables]
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
private EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
public IReadOnlyList<EntityUid> StoredEntities => Container.ContainedEntities;
@@ -254,70 +254,7 @@ namespace Content.Server.Cuffs.Components
if (result != DoAfterStatus.Cancelled)
{
SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
_entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>().DeleteInHandsMatching(user, cuffsToRemove.Value);
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove.Value);
if (cuff.BreakOnRemove)
{
cuff.Broken = true;
var meta = _entMan.GetComponent<MetaDataComponent>(cuffsToRemove.Value);
meta.EntityName = cuff.BrokenName;
meta.EntityDescription = cuff.BrokenDesc;
if (_entMan.TryGetComponent<SpriteComponent?>(cuffsToRemove, out var sprite) && cuff.BrokenState != null)
{
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
}
_entMan.AddComponent<RecyclableComponent>(cuffsToRemove.Value);
}
CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount;
_entMan.EntitySysManager.GetEntitySystem<ActionBlockerSystem>().UpdateCanMove(Owner);
var ev = new CuffedStateChangeEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true);
UpdateAlert();
Dirty(_entMan);
if (CuffedHandCount == 0)
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message"));
if (!isOwner)
{
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user)));
}
if (user == Owner)
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves");
}
else
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}");
}
}
else
{
if (!isOwner)
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
("cuffedHandCount", CuffedHandCount),
("otherName", user)));
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message",
("otherName", user),
("cuffedHandCount", CuffedHandCount)));
}
else
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount)));
}
}
Uncuff(user, cuffsToRemove.Value, cuff, isOwner);
}
else
{
@@ -326,5 +263,71 @@ namespace Content.Server.Cuffs.Components
return;
}
//Lord forgive me for putting this here
//Cuff ECS when
public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner)
{
SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
_entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>().DeleteInHandsMatching(user, cuffsToRemove);
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove);
if (cuff.BreakOnRemove)
{
cuff.Broken = true;
var meta = _entMan.GetComponent<MetaDataComponent>(cuffsToRemove);
meta.EntityName = cuff.BrokenName;
meta.EntityDescription = cuff.BrokenDesc;
if (_entMan.TryGetComponent<SpriteComponent>(cuffsToRemove, out var sprite) && cuff.BrokenState != null)
{
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
}
_entMan.AddComponent<RecyclableComponent>(cuffsToRemove);
}
CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount;
_entMan.EntitySysManager.GetEntitySystem<ActionBlockerSystem>().UpdateCanMove(Owner);
var ev = new CuffedStateChangeEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true);
UpdateAlert();
Dirty(_entMan);
if (CuffedHandCount == 0)
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message"));
if (!isOwner)
{
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user)));
}
if (user == Owner)
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves");
}
else
{
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}");
}
}
else
{
if (!isOwner)
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount), ("otherName", user)));
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", ("otherName", user), ("cuffedHandCount", CuffedHandCount)));
}
else
{
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount)));
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Gibs on trigger, self explanatory.
/// Also in case of an implant using this, gibs the implant user instead.
/// </summary>
[RegisterComponent]
public sealed class GibOnTriggerComponent : Component
{
/// <summary>
/// Should gibbing also delete the owners items?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("deleteItems")]
public bool DeleteItems = false;
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.MobState;
using Robust.Shared.Audio;
namespace Content.Server.Explosion.Components

View File

@@ -0,0 +1,16 @@
using Content.Shared.MobState;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Use where you want something to trigger on mobstate change
/// </summary>
[RegisterComponent]
public sealed class TriggerOnMobstateChangeComponent : Component
{
/// <summary>
/// What state should trigger this?
/// </summary>
[DataField("mobState", required: true)]
public DamageState MobState = DamageState.Alive;
}

View File

@@ -3,6 +3,7 @@ using Content.Server.Sticky.Events;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Interaction.Events;
using Content.Shared.MobState;
using Content.Shared.Verbs;
using Robust.Shared.Player;

View File

@@ -1,11 +1,13 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Explosion.Components;
using Content.Server.Flash;
using Content.Server.Flash.Components;
using Content.Server.Sticky.Events;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Physics;
@@ -14,7 +16,9 @@ using Robust.Shared.Player;
using Content.Shared.Trigger;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.MobState;
using Content.Shared.Payload.Components;
using Content.Shared.StepTrigger.Systems;
using Robust.Server.Containers;
@@ -48,6 +52,7 @@ namespace Content.Server.Explosion.EntitySystems
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly BodySystem _body = default!;
public override void Initialize()
{
@@ -61,11 +66,14 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredEvent>(OnStepTriggered);
SubscribeLocalEvent<TriggerOnMobstateChangeComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(HandleExplodeTrigger);
SubscribeLocalEvent<FlashOnTriggerComponent, TriggerEvent>(HandleFlashTrigger);
SubscribeLocalEvent<GibOnTriggerComponent, TriggerEvent>(HandleGibTrigger);
}
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
@@ -89,6 +97,17 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void HandleGibTrigger(EntityUid uid, GibOnTriggerComponent component, TriggerEvent args)
{
if (!TryComp<TransformComponent>(uid, out var xform))
return;
_body.GibBody(xform.ParentUid, deleteItems: component.DeleteItems);
args.Handled = true;
}
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args)
{
if(args.OurFixture.ID == component.FixtureID)
@@ -101,11 +120,39 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
{
Trigger(uid);
}
private void OnStepTriggered(EntityUid uid, TriggerOnStepTriggerComponent component, ref StepTriggeredEvent args)
{
Trigger(uid, args.Tripper);
}
private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent component, MobStateChangedEvent args)
{
if (component.MobState < args.CurrentMobState)
return;
//This chains Mobstate Changed triggers with OnUseTimerTrigger if they have it
//Very useful for things that require a mobstate change and a timer
if (TryComp<OnUseTimerTriggerComponent>(uid, out var timerTrigger))
{
HandleTimerTrigger(
uid,
args.Origin,
timerTrigger.Delay,
timerTrigger.BeepInterval,
timerTrigger.InitialBeepDelay,
timerTrigger.BeepSound,
timerTrigger.BeepParams);
}
else
Trigger(uid);
}
public bool Trigger(EntityUid trigger, EntityUid? user = null)
{
var triggerEvent = new TriggerEvent(trigger, user);

View File

@@ -0,0 +1,25 @@
using Content.Shared.Implants.Components;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
public sealed partial class ImplanterSystem
{
public void InitializeImplanted()
{
SubscribeLocalEvent<ImplantedComponent, ComponentInit>(OnImplantedInit);
SubscribeLocalEvent<ImplantedComponent, ComponentShutdown>(OnShutdown);
}
private void OnImplantedInit(EntityUid uid, ImplantedComponent component, ComponentInit args)
{
component.ImplantContainer = _container.EnsureContainer<Container>(uid, ImplanterComponent.ImplantSlotId);
component.ImplantContainer.OccludesLight = false;
}
private void OnShutdown(EntityUid uid, ImplantedComponent component, ComponentShutdown args)
{
//If the entity is deleted, get rid of the implants
_container.CleanContainer(component.ImplantContainer);
}
}

View File

@@ -0,0 +1,188 @@
using System.Threading;
using Content.Server.DoAfter;
using Content.Server.Guardian;
using Content.Server.Popups;
using Content.Shared.Hands;
using Content.Shared.IdentityManagement;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.MobState.Components;
using Content.Shared.Popups;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Server.Implants;
public sealed partial class ImplanterSystem : SharedImplanterSystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
InitializeImplanted();
SubscribeLocalEvent<ImplanterComponent, HandDeselectedEvent>(OnHandDeselect);
SubscribeLocalEvent<ImplanterComponent, AfterInteractEvent>(OnImplanterAfterInteract);
SubscribeLocalEvent<ImplanterComponent, ComponentGetState>(OnImplanterGetState);
SubscribeLocalEvent<ImplanterComponent, ImplanterImplantCompleteEvent>(OnImplantAttemptSuccess);
SubscribeLocalEvent<ImplanterComponent, ImplanterDrawCompleteEvent>(OnDrawAttemptSuccess);
SubscribeLocalEvent<ImplanterComponent, ImplanterCancelledEvent>(OnImplantAttemptFail);
}
private void OnImplanterAfterInteract(EntityUid uid, ImplanterComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || args.Handled)
return;
if (component.CancelToken != null)
{
args.Handled = true;
return;
}
//Simplemobs and regular mobs should be injectable, but only regular mobs have mind.
//So just don't implant/draw anything that isn't living or is a guardian
//TODO: Rework a bit when surgery is in to work with implant cases
if (!HasComp<MobStateComponent>(args.Target.Value) || HasComp<GuardianComponent>(args.Target.Value))
return;
//TODO: Rework when surgery is in for implant cases
if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly)
{
TryDraw(component, args.User, args.Target.Value, uid);
}
else
{
//Implant self instantly, otherwise try to inject the target.
if (args.User == args.Target)
Implant(uid, args.Target.Value, component);
else
TryImplant(component, args.User, args.Target.Value, uid);
}
args.Handled = true;
}
private void OnHandDeselect(EntityUid uid, ImplanterComponent component, HandDeselectedEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
}
/// <summary>
/// Attempt to implant someone else.
/// </summary>
/// <param name="component">Implanter component</param>
/// <param name="user">The entity using the implanter</param>
/// <param name="target">The entity being implanted</param>
/// <param name="implanter">The implanter being used</param>
public void TryImplant(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter)
{
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, Filter.Entities(target), PopupType.LargeCaution);
component.CancelToken?.Cancel();
component.CancelToken = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(user, component.ImplantTime, component.CancelToken.Token, target, implanter)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
BreakOnDamage = true,
BreakOnStun = true,
UsedFinishedEvent = new ImplanterImplantCompleteEvent(implanter, target),
UserCancelledEvent = new ImplanterCancelledEvent()
});
}
/// <summary>
/// Try to remove an implant and store it in an implanter
/// </summary>
/// <param name="component">Implanter component</param>
/// <param name="user">The entity using the implanter</param>
/// <param name="target">The entity getting their implant removed</param>
/// <param name="implanter">The implanter being used</param>
//TODO: Remove when surgery is in
public void TryDraw(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter)
{
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
component.CancelToken?.Cancel();
component.CancelToken = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(user, component.DrawTime, component.CancelToken.Token, target ,implanter)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
BreakOnDamage = true,
BreakOnStun = true,
UsedFinishedEvent = new ImplanterDrawCompleteEvent(implanter, user, target),
UsedCancelledEvent = new ImplanterCancelledEvent()
});
}
private void OnImplanterGetState(EntityUid uid, ImplanterComponent component, ref ComponentGetState args)
{
args.State = new ImplanterComponentState(component.CurrentMode, component.ImplantOnly);
}
private void OnImplantAttemptSuccess(EntityUid uid, ImplanterComponent component, ImplanterImplantCompleteEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
Implant(args.Implanter, args.Target, component);
}
private void OnDrawAttemptSuccess(EntityUid uid, ImplanterComponent component, ImplanterDrawCompleteEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
Draw(args.Implanter, args.User, args.Target, component);
}
private void OnImplantAttemptFail(EntityUid uid, ImplanterComponent component, ImplanterCancelledEvent args)
{
component.CancelToken?.Cancel();
component.CancelToken = null;
}
private sealed class ImplanterImplantCompleteEvent : EntityEventArgs
{
public EntityUid Implanter;
public EntityUid Target;
public ImplanterImplantCompleteEvent(EntityUid implanter, EntityUid target)
{
Implanter = implanter;
Target = target;
}
}
private sealed class ImplanterCancelledEvent : EntityEventArgs
{
}
private sealed class ImplanterDrawCompleteEvent : EntityEventArgs
{
public EntityUid Implanter;
public EntityUid User;
public EntityUid Target;
public ImplanterDrawCompleteEvent(EntityUid implanter, EntityUid user, EntityUid target)
{
Implanter = implanter;
User = user;
Target = target;
}
}
}

View File

@@ -0,0 +1,58 @@
using Content.Server.Cuffs.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.MobState;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, UseFreedomImplantEvent>(OnFreedomImplant);
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
}
private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args)
{
if (!TryComp<CuffableComponent>(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
return;
if (TryComp<HandcuffComponent>(cuffs.LastAddedCuffs, out var cuff))
{
cuffs.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuff, true);
}
}
#region Relays
//Relays from the implanted to the implant
private void RelayToImplantEvent<T>(EntityUid uid, ImplantedComponent component, T args) where T : EntityEventArgs
{
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
return;
foreach (var implant in implantContainer.ContainedEntities)
{
RaiseLocalEvent(implant, args);
}
}
//Relays from the implant to the implanted
private void RelayToImplantedEvent<T>(EntityUid uid, SubdermalImplantComponent component, T args) where T : EntityEventArgs
{
if (component.ImplantedEntity != null)
{
RaiseLocalEvent(component.ImplantedEntity.Value, args);
}
}
#endregion
}

View File

@@ -0,0 +1,39 @@
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Jobs;
/// <summary>
/// Adds implants on spawn to the entity
/// </summary>
[UsedImplicitly]
public sealed class AddImplantSpecial : JobSpecial
{
[DataField("implants", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<EntityPrototype>))]
public HashSet<String> Implants { get; } = new();
public override void AfterEquip(EntityUid mob)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var implantSystem = entMan.System<SharedSubdermalImplantSystem>();
var xformQuery = entMan.GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(mob, out var xform))
return;
foreach (var implantId in Implants)
{
var implant = entMan.SpawnEntity(implantId, xform.Coordinates);
if (!entMan.TryGetComponent<SubdermalImplantComponent>(implant, out var implantComp))
return;
implantSystem.ForceImplant(mob, implant, implantComp);
}
}
}

View File

@@ -34,6 +34,12 @@ namespace Content.Server.Medical.SuitSensors
[DataField("activationSlot")]
public string ActivationSlot = "jumpsuit";
/// <summary>
/// Activate sensor if user has this in a sensor-compatible container.
/// </summary>
[DataField("activationContainer")]
public string? ActivationContainer;
/// <summary>
/// How often does sensor update its owners status (in seconds). Limited by the system update rate.
/// </summary>
@@ -43,6 +49,7 @@ namespace Content.Server.Medical.SuitSensors
/// <summary>
/// Current user that wears suit sensor. Null if nobody wearing it.
/// </summary>
[ViewVariables]
public EntityUid? User = null;
/// <summary>

View File

@@ -9,6 +9,7 @@ using Content.Shared.Inventory.Events;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.MobState.Components;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
@@ -35,6 +36,8 @@ namespace Content.Server.Medical.SuitSensors
SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
public override void Update(float frameTime)
@@ -150,6 +153,22 @@ namespace Content.Server.Medical.SuitSensors
});
}
private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = args.Container.Owner;
}
private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = null;
}
private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
{
return new Verb()

View File

@@ -29,6 +29,7 @@ using Content.Shared.Destructible;
using static Content.Shared.Storage.SharedStorageComponent;
using Content.Shared.ActionBlocker;
using Content.Shared.CombatMode;
using Content.Shared.Implants.Components;
using Content.Shared.Movement.Events;
namespace Content.Server.Storage.EntitySystems
@@ -62,6 +63,7 @@ namespace Content.Server.Storage.EntitySystems
SubscribeLocalEvent<ServerStorageComponent, GetVerbsEvent<UtilityVerb>>(AddTransferVerbs);
SubscribeLocalEvent<ServerStorageComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ServerStorageComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<ServerStorageComponent, OpenStorageImplantEvent>(OnImplantActivate);
SubscribeLocalEvent<ServerStorageComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<ServerStorageComponent, DestructionEventArgs>(OnDestroy);
SubscribeLocalEvent<ServerStorageComponent, StorageInteractWithItemEvent>(OnInteractWithItem);
@@ -277,6 +279,17 @@ namespace Content.Server.Storage.EntitySystems
OpenStorageUI(uid, args.User, storageComp);
}
/// <summary>
/// Specifically for storage implants.
/// </summary>
private void OnImplantActivate(EntityUid uid, ServerStorageComponent storageComp, OpenStorageImplantEvent args)
{
if (args.Handled || !TryComp<TransformComponent>(uid, out var xform))
return;
OpenStorageUI(uid, xform.ParentUid, storageComp);
}
/// <summary>
/// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius
/// around a click.

View File

@@ -162,7 +162,7 @@ public partial class SharedBodySystem
}
public virtual HashSet<EntityUid> GibBody(EntityUid? partId, bool gibOrgans = false,
BodyComponent? body = null)
BodyComponent? body = null, bool deleteItems = false)
{
if (partId == null || !Resolve(partId.Value, ref body, false))
return new HashSet<EntityUid>();

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Containers;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Added to an entity via the <see cref="SharedImplanterSystem"/> on implant
/// Used in instances where mob info needs to be passed to the implant such as MobState triggers
/// </summary>
[RegisterComponent]
public sealed class ImplantedComponent : Component
{
public Container ImplantContainer = default!;
}

View File

@@ -0,0 +1,107 @@
using System.Threading;
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Implanters are used to implant or extract implants from an entity
/// Some can be single use (implant only) or some can draw out an implant
/// </summary>
//TODO: Rework drawing to work with implant cases when surgery is in
[RegisterComponent, NetworkedComponent]
public sealed class ImplanterComponent : Component
{
public const string ImplanterSlotId = "implanter_slot";
public const string ImplantSlotId = "implant";
/// <summary>
/// Used for implanters that start with specific implants
/// </summary>
[ViewVariables]
[DataField("implant", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Implant;
/// <summary>
/// The time it takes to implant someone else
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("implantTime")]
public float ImplantTime = 5f;
//TODO: Remove when surgery is a thing
/// <summary>
/// The time it takes to extract an implant from someone
/// It's excessively long to deter from implant checking any antag
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("drawTime")]
public float DrawTime = 300f;
/// <summary>
/// Good for single-use injectors
/// </summary>
[ViewVariables]
[DataField("implantOnly")]
public bool ImplantOnly = false;
/// <summary>
/// The current mode of the implanter
/// Mode is changed automatically depending if it implants or draws
/// </summary>
[ViewVariables]
[DataField("currentMode")]
public ImplanterToggleMode CurrentMode;
/// <summary>
/// The name and description of the implant to show on the implanter
/// </summary>
[ViewVariables]
[DataField("implantData")]
public (string, string) ImplantData;
/// <summary>
/// The <see cref="ItemSlot"/> for this implanter
/// </summary>
[ViewVariables]
[DataField("implanterSlot")]
public ItemSlot ImplanterSlot = new();
public bool UiUpdateNeeded;
public CancellationTokenSource? CancelToken;
}
[Serializable, NetSerializable]
public sealed class ImplanterComponentState : ComponentState
{
public ImplanterToggleMode CurrentMode;
public bool ImplantOnly;
public ImplanterComponentState(ImplanterToggleMode currentMode, bool implantOnly)
{
CurrentMode = currentMode;
ImplantOnly = implantOnly;
}
}
[Serializable, NetSerializable]
public enum ImplanterToggleMode : byte
{
Inject,
Draw
}
[Serializable, NetSerializable]
public enum ImplanterVisuals : byte
{
Full
}
[Serializable, NetSerializable]
public enum ImplanterImplantOnlyVisuals : byte
{
ImplantOnly
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Actions;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Subdermal implants get stored in a container on an entity and grant the entity special actions
/// The actions can be activated via an action, a passive ability (ie tracking), or a reactive ability (ie on death) or some sort of combination
/// They're added and removed with implanters
/// </summary>
[RegisterComponent]
public sealed class SubdermalImplantComponent : Component
{
/// <summary>
/// Used where you want the implant to grant the owner an instant action.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("implantAction")]
public string? ImplantAction;
/// <summary>
/// The entity this implant is inside
/// </summary>
[ViewVariables]
public EntityUid? ImplantedEntity;
/// <summary>
/// Should this implant be removeable?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("permanent")]
public bool Permanent = false;
}
/// <summary>
/// Used for opening the storage implant via action.
/// </summary>
public sealed class OpenStorageImplantEvent : InstantActionEvent
{
}
public sealed class UseFreedomImplantEvent : InstantActionEvent
{
}
/// <summary>
/// Used for triggering trigger events on the implant via action
/// </summary>
public sealed class ActivateImplantEvent : InstantActionEvent
{
}

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Implants.Components;
/// <summary>
/// Triggers implants when the action is pressed
/// </summary>
[RegisterComponent]
public sealed class TriggerImplantActionComponent : Component
{
}

View File

@@ -0,0 +1,156 @@
using System.Linq;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.IdentityManagement;
using Content.Shared.Implants.Components;
using Content.Shared.Popups;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Shared.Implants;
public abstract class SharedImplanterSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ImplanterComponent, ComponentInit>(OnImplanterInit);
SubscribeLocalEvent<ImplanterComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
}
private void OnImplanterInit(EntityUid uid, ImplanterComponent component, ComponentInit args)
{
if (component.Implant != null)
component.ImplanterSlot.StartingItem = component.Implant;
_itemSlots.AddItemSlot(uid, ImplanterComponent.ImplanterSlotId, component.ImplanterSlot);
}
private void OnEntInserted(EntityUid uid, ImplanterComponent component, EntInsertedIntoContainerMessage args)
{
var implantData = EntityManager.GetComponent<MetaDataComponent>(args.Entity);
component.ImplantData = (implantData.EntityName, implantData.EntityDescription);
}
//Instantly implant something and add all necessary components and containers.
//Set to draw mode if not implant only
public void Implant(EntityUid implanter, EntityUid target, ImplanterComponent component)
{
var implanterContainer = component.ImplanterSlot.ContainerSlot;
if (implanterContainer is null)
return;
var implant = implanterContainer.ContainedEntities.FirstOrDefault();
if (!TryComp<SubdermalImplantComponent>(implant, out var implantComp))
return;
//If the target doesn't have the implanted component, add it.
var implantedComp = EnsureComp<ImplantedComponent>(target);
var implantContainer = implantedComp.ImplantContainer;
implanterContainer.Remove(implant);
implantComp.ImplantedEntity = target;
implantContainer.OccludesLight = false;
implantContainer.Insert(implant);
if (component.CurrentMode == ImplanterToggleMode.Inject && !component.ImplantOnly)
DrawMode(component);
else
ImplantMode(component);
Dirty(component);
}
//Draw the implant out of the target
//TODO: Rework when surgery is in so implant cases can be a thing
public void Draw(EntityUid implanter, EntityUid user, EntityUid target, ImplanterComponent component)
{
var implanterContainer = component.ImplanterSlot.ContainerSlot;
if (implanterContainer is null)
return;
var permanentFound = false;
if (_container.TryGetContainer(target, ImplanterComponent.ImplantSlotId, out var implantContainer))
{
var implantCompQuery = GetEntityQuery<SubdermalImplantComponent>();
foreach (var implant in implantContainer.ContainedEntities)
{
if (!implantCompQuery.TryGetComponent(implant, out var implantComp))
return;
//Don't remove a permanent implant and look for the next that can be drawn
if (!implantContainer.CanRemove(implant))
{
var implantName = Identity.Entity(implant, EntityManager);
var targetName = Identity.Entity(target, EntityManager);
var failedPermanentMessage = Loc.GetString("implanter-draw-failed-permanent", ("implant", implantName), ("target", targetName));
_popup.PopupEntity(failedPermanentMessage, target, Filter.Entities(user));
permanentFound = implantComp.Permanent;
continue;
}
implantContainer.Remove(implant);
implantComp.ImplantedEntity = null;
implanterContainer.Insert(implant);
permanentFound = implantComp.Permanent;
//Break so only one implant is drawn
break;
}
if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound)
ImplantMode(component);
Dirty(component);
}
}
private void ImplantMode(ImplanterComponent component)
{
component.CurrentMode = ImplanterToggleMode.Inject;
ChangeOnImplantVisualizer(component);
}
private void DrawMode(ImplanterComponent component)
{
component.CurrentMode = ImplanterToggleMode.Draw;
ChangeOnImplantVisualizer(component);
}
private void ChangeOnImplantVisualizer(ImplanterComponent component)
{
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance))
return;
bool implantFound;
if (component.ImplanterSlot.HasItem)
implantFound = true;
else
implantFound = false;
if (component.CurrentMode == ImplanterToggleMode.Inject && !component.ImplantOnly)
_appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance);
else if (component.CurrentMode == ImplanterToggleMode.Inject && component.ImplantOnly)
{
_appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance);
_appearance.SetData(component.Owner, ImplanterImplantOnlyVisuals.ImplantOnly, component.ImplantOnly, appearance);
}
else
_appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance);
}
}

View File

@@ -0,0 +1,120 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Implants.Components;
using Content.Shared.Tag;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Shared.Implants;
public abstract class SharedSubdermalImplantSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly TagSystem _tag = default!;
public const string BaseStorageId = "storagebase";
public override void Initialize()
{
SubscribeLocalEvent<SubdermalImplantComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SubdermalImplantComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
SubscribeLocalEvent<SubdermalImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args)
{
if (component.ImplantedEntity == null)
return;
if (component.ImplantAction != null)
{
var action = new InstantAction(_prototypeManager.Index<InstantActionPrototype>(component.ImplantAction));
_actionsSystem.AddAction(component.ImplantedEntity.Value, action, uid);
}
//replace micro bomb with macro bomb
if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, "MacroBomb"))
{
foreach (var implant in implantContainer.ContainedEntities)
{
if (_tag.HasTag(implant, "MicroBomb"))
{
implantContainer.Remove(implant);
QueueDel(implant);
}
}
}
}
private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args)
{
if (component.Permanent && component.ImplantedEntity != null)
args.Cancel();
}
private void OnRemove(EntityUid uid, SubdermalImplantComponent component, EntGotRemovedFromContainerMessage args)
{
if (component.ImplantedEntity == null)
return;
var entCoords = Transform(component.ImplantedEntity.Value).Coordinates;
if (component.ImplantAction != null)
_actionsSystem.RemoveProvidedActions(component.ImplantedEntity.Value, uid);
if (!_container.TryGetContainer(uid, BaseStorageId, out var storageImplant))
return;
_container.EmptyContainer(storageImplant, moveTo: entCoords);
}
/// <summary>
/// Forces an implant into a person
/// Good for on spawn related code or admin additions
/// </summary>
/// <param name="target">The entity to be implanted</param>
/// <param name="implant"> The implant</param>
/// <param name="component">The implant component</param>
public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component)
{
//If the target doesn't have the implanted component, add it.
var implantedComp = EnsureComp<ImplantedComponent>(target);
var implantContainer = implantedComp.ImplantContainer;
component.ImplantedEntity = target;
implantContainer.Insert(implant);
}
/// <summary>
/// Force remove a singular implant
/// </summary>
/// <param name="target">the implanted entity</param>
/// <param name="implant">the implant</param>
/// <param name="component">the implant component</param>
public void ForceRemove(EntityUid target, EntityUid implant)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
return;
var implantContainer = implanted.ImplantContainer;
implantContainer.Remove(implant);
QueueDel(implant);
}
/// <summary>
/// Removes and deletes implants by force
/// </summary>
/// <param name="target">The entity to have implants removed</param>
public void WipeImplants(EntityUid target)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
return;
var implantContainer = implanted.ImplantContainer;
_container.CleanContainer(implantContainer);
}
}

View File

@@ -32,6 +32,11 @@ bite.ogg take from https://github.com/tgstation/tgstation/commit/d4f678a1772007f
bone_rattle.ogg licensed under CC0 1.0 and taken from spookymodem at https://freesound.org/people/spookymodem/sounds/202102/
- files: ["sadtrombone.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "sadtrombone.ogg taken from Citadel Station."
source: "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/35a1723e98a60f375df590ca572cc90f1bb80bd5"
The following sounds are taken from TGstation github (licensed under CC by 3.0):
demon_attack1.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0

Binary file not shown.

View File

@@ -0,0 +1,25 @@
## Implanter Attempt Messages
implanter-component-implanting-target = {$user} is trying to implant you with something!
implanter-draw-failed-permanent = The {$implant} in {$target} is fused with them and cannot be removed!
implanter-draw-failed = You tried to remove an implant but found nothing.
## UI
implanter-draw-text = Draw
implanter-inject-text = Inject
implanter-empty-text = None
implanter-implant-text = {$implantName}{$lineBreak}{$implantDescription}
implanter-label = [color=white]Implant: {$currentEntities}{$lineBreak}Mode: {$modeString}[/color]
## Implanter Actions
open-storage-implant-action-name = open storage implant
open-storage-implant-action-description = opens the storage implant embedded under your skin
activate-micro-bomb-action-name = activate micro bomb
activate-micro-bomb-action-description = activates your internal microbomb, completely destroying you and your equipment
use-freedom-implant-action-name = use freedom implant
use-freedom-implant-action-description = activating the implant will free you from any hand restraints

View File

@@ -4,5 +4,8 @@ ent-ArmorySmg = { ent-CrateArmorySMG }
ent-ArmoryShotgun = { ent-CrateArmoryShotgun }
.desc = { ent-CrateArmoryShotgun.desc }
ent-TrackingImplants = { ent-CrateTrackingImplants }
.desc = { ent-CrateTrackingImplants.desc }
ent-ArmoryLaser = { ent-CrateArmoryLaser }
.desc = { ent-CrateArmoryLaser.desc }
.desc = { ent-CrateArmoryLaser.desc }

View File

@@ -4,5 +4,8 @@ ent-CrateArmorySMG = SMG crate
ent-CrateArmoryShotgun = Shotgun crate
.desc = For when the enemy absolutely needs to be replaced with lead. Contains two Enforcer Combat Shotguns, and some standard shotgun shells. Requires Armory access to open.
ent-CrateTrackingImplants = Tracking implants
.desc = Contains a handful of tracking implanters. Good for prisoners you'd like to release but still keep track of.
ent-CrateArmoryLaser = lasers crate
.desc = Contains three lethal, high-energy laser guns. Requires Armory access to open.
.desc = Contains three lethal, high-energy laser guns. Requires Armory access to open.

View File

@@ -16,5 +16,11 @@ ent-CrateFunBoardGames = Board game crate
ent-CrateFunATV = ATV crate
.desc = An Absolutely Taxable Vehicle to help cargo with hauling.
ent-CrateFunSadTromboneImplants = Sad Trombone Implants
.desc = Death's never been so fun before! Implant these to make dying a bit more happy.
ent-CrateFunLightImplants = Light Implants
.desc = Light up your skin with these implants!
ent-CrateFunSyndicateSegway = Syndicate segway crate
.desc = A crate containing a two-wheeler that will help you escape from the security officers. Or not.

View File

@@ -22,6 +22,39 @@
iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
event: !type:ToggleActionEvent
- type: instantAction
id: OpenStorageImplant
name: open-storage-implant-action-name
description: open-storage-implant-action-description
itemIconStyle: BigAction
icon:
sprite: Clothing/Back/Backpacks/backpack.rsi
state: icon
event: !type:OpenStorageImplantEvent
- type: instantAction
id: ActivateMicroBomb
name: activate-micro-bomb-action-name
description: activate-micro-bomb-action-description
checkCanInteract: false
itemIconStyle: BigAction
icon:
sprite: Actions/Implants/implants.rsi
state: explosive
event: !type:ActivateImplantEvent
- type: instantAction
id: ActivateFreedomImplant
name: use-freedom-implant-action-name
description: use-freedom-implant-action-description
charges: 3
checkCanInteract: false
itemIconStyle: BigAction
icon:
sprite: Actions/Implants/implants.rsi
state: freedom
event: !type:UseFreedomImplantEvent
- type: instantAction
id: ToggleSuitHelmet
name: action-name-hardsuit

View File

@@ -18,6 +18,16 @@
category: Armory
group: market
- type: cargoProduct
id: TrackingImplant
icon:
sprite: Objects/Specific/Chemistry/syringe.rsi
state: syringe_base0
product: CrateTrackingImplants
cost: 1000
category: Armory
group: market
- type: cargoProduct
id: ArmoryLaser
icon:
@@ -26,4 +36,4 @@
product: CrateArmoryLaser
cost: 1600
category: Armory
group: market
group: market

View File

@@ -57,3 +57,23 @@
cost: 1500
category: Fun
group: market
- type: cargoProduct
id: FunSadTromboneImplants
icon:
sprite: Objects/Specific/Chemistry/syringe.rsi
state: syringe_base0
product: CrateFunSadTromboneImplants
cost: 1000
category: Fun
group: market
- type: cargoProduct
id: FunLightImplants
icon:
sprite: Objects/Specific/Chemistry/syringe.rsi
state: syringe_base0
product: CrateFunLightImplants
cost: 1000
category: Fun
group: market

View File

@@ -20,6 +20,15 @@
- id: BoxLethalshot
amount: 3
- type: entity
id: CrateTrackingImplants
parent: CrateWeaponSecure
components:
- type: StorageFill
contents:
- id: TrackingImplanter
amount: 5
- type: entity
id: CrateArmoryLaser
parent: CrateWeaponSecure
@@ -27,4 +36,4 @@
- type: StorageFill
contents:
- id: WeaponLaserGun
amount: 3
amount: 3

View File

@@ -118,6 +118,24 @@
- id: VehicleKeyATV
amount: 1
- type: entity
id: CrateFunSadTromboneImplants
parent: CrateGenericSteel
components:
- type: StorageFill
contents:
- id: SadTromboneImplanter
amount: 3
- type: entity
id: CrateFunLightImplants
parent: CrateGenericSteel
components:
- type: StorageFill
contents:
- id: LightImplanter
amount: 3
- type: entity
id: CrateFunSyndicateSegway
parent: CrateLivestock

View File

@@ -51,6 +51,7 @@
- ClothingHandsGlovesNitrile
- ClothingMaskSterile
- DiseaseSwab
- Implanter
- type: technology
name: technologies-advanced-botany

View File

@@ -280,6 +280,56 @@
categories:
- UplinkUtility
# Utility-Implants
- type: listing
id: UplinkStorageImplanter
icon: /Textures/Clothing/Back/Backpacks/backpack.rsi/icon.png
name: Storage Implanter
description: Hide goodies inside of yourself with new bluespace technology!
productEntity: StorageImplanter
cost:
Telecrystal: 12
categories:
- UplinkUtility
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- type: listing
id: UplinkFreedomImplanter
icon: /Textures/Actions/Implants/implants.rsi/freedom.png
name: Freedom Implanter
description: Get away from those nasty sec officers with this three use implant!
productEntity: FreedomImplanter
cost:
Telecrystal: 10
categories:
- UplinkUtility
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- NukeOpsUplink
- type: listing
id: UplinkMacroBombImplanter
icon: /Textures/Actions/Implants/implants.rsi/explosive.png
name: Macro Bomb Implanter
description: Inject this and on death you'll create a large explosion. Huge team casualty cost, use at own risk. Replaces internal micro bomb.
productEntity: MacroBombImplanter
cost:
Telecrystal: 20
categories:
- UplinkUtility
conditions:
- !type:StoreWhitelistCondition
blacklist:
tags:
- TraitorUplink
# Bundles
- type: listing

View File

@@ -0,0 +1,146 @@
# Implanters
- type: entity
name: implanter
description: a syringe fitted to be used exclusively with implants
id: BaseImplanter
parent: BaseItem
abstract: true
components:
- type: ItemSlots
- type: ContainerContainer
containers:
implanter_slot: !type:ContainerSlot { }
- type: Implanter
currentMode: Draw
implanterSlot:
name: Implant
priority: 0
whitelist:
tags:
- SubdermalImplant
- type: Sprite
sprite: Objects/Specific/Chemistry/syringe.rsi
netsync: false
state: syringe_base0
layers:
- state: syringe4
map: [ "implantFull" ]
color: '#1cd94e'
visible: false
- state: syringe_base0
map: [ "implantOnly" ]
- type: Item
sprite: Objects/Specific/Chemistry/syringe.rsi
heldPrefix: 0
- type: Appearance
- type: GenericVisualizer
visuals:
enum.ImplanterVisuals.Full:
implantFull:
True: {visible: true}
False: {visible: false}
enum.ImplanterImplantOnlyVisuals.ImplantOnly:
implantOnly:
True: {state: broken}
False: {state: syringe_base0}
- type: entity
id: Implanter
parent: BaseImplanter
components:
- type: Tag
tags:
- Trash
- type: entity
id: BaseImplantOnlyImplanter
parent: Implanter
description: a single use implanter
abstract: true
components:
- type: Sprite
sprite: Objects/Specific/Chemistry/syringe.rsi
netsync: false
state: syringe_base0
layers:
- state: syringe4
map: [ "implantFull" ]
color: '#1cd94e'
visible: true
- state: syringe_base0
map: [ "implantOnly" ]
- type: Implanter
currentMode: Inject
implantOnly: true
#Fun implanters
- type: entity
id: SadTromboneImplanter
name: sad trombone implanter
description: a single use implanter, the implant plays a sad tune on death
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: SadTromboneImplant
- type: entity
id: LightImplanter
name: light implanter
description: a single use implanter, the implant emits light on activation
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: LightImplant
#Security implanters
- type: entity
id: TrackingImplanter
name: tracking implanter
description: a single use implanter, the implant tracks
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: TrackingImplant
#Traitor implanters
- type: entity
id: StorageImplanter
name: storage implanter
description: a single use implanter, the implant grants hidden storage
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: StorageImplant
- type: entity
id: FreedomImplanter
name: freedom implanter
description: a single use implanter, the implant lets the user break out of hand restraints three times
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: FreedomImplant
#Nuclear Operative/Special implanters
- type: entity
id: MicroBombImplanter
name: micro bomb implanter
description: a single use implanter, the implant is permanent and blows the user up on death
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: MicroBombImplant
- type: entity
id: MacroBombImplanter
name: macro bomb implanter
description: a single use implanter, the implant creates a large explosion on death after the alloted time
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: MacroBombImplant

View File

@@ -0,0 +1,174 @@
- type: entity
parent: BaseItem
id: BaseSubdermalImplant
name: implant
description: a microscopic chip that's injected under the skin
abstract: true
components:
- type: SubdermalImplant
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
#Fun implants
- type: entity
parent: BaseSubdermalImplant
id: SadTromboneImplant
name: sad trombone implant
description: plays a sad tune when the user dies
noSpawn: true
components:
- type: SubdermalImplant
- type: TriggerOnMobstateChange
mobState: Dead
- type: EmitSoundOnTrigger
sound:
collection: SadTrombone
params:
variation: 0.125
- type: entity
parent: BaseSubdermalImplant
id: LightImplant
name: light implant
description: makes your skin emit a faint light
noSpawn: true
components:
- type: SubdermalImplant
implantAction: ToggleLight
- type: PointLight
enabled: false
radius: 2.5
softness: 5
mask: /Textures/Effects/LightMasks/cone.png
autoRot: true
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- Flashlight
- type: UnpoweredFlashlight
toggleAction:
name: action-name-toggle-light
description: action-description-toggle-light
icon: Objects/Tools/flashlight.rsi/flashlight.png
iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png
event: !type:ToggleActionEvent
#Security implants
- type: entity
parent: BaseSubdermalImplant
id: TrackingImplant
name: tracking implant
description: tracks whoever was implanted via the suit sensor network
noSpawn: true
components:
- type: SubdermalImplant
- type: SuitSensor
randomMode: false
controlsLocked: true
mode: SensorCords
activationContainer: "ImplantContainer"
- type: DeviceNetwork
deviceNetId: Wireless
transmitFrequencyId: SuitSensor
- type: WirelessNetworkConnection
range: 500
#Traitor implants
- type: entity
parent: BaseSubdermalImplant
id: StorageImplant
name: storage implant
description: made with bluespace technology, allows the user to fit a few items in hidden storage
noSpawn: true
components:
- type: SubdermalImplant
implantAction: OpenStorageImplant
- type: Item
size: 9999
- type: Storage
capacity: 20 #10-20 should be more than enough for this
- type: ContainerContainer
containers:
storagebase: !type:Container
ents: [ ]
- type: UserInterface
interfaces:
- key: enum.StorageUiKey.Key
type: StorageBoundUserInterface
- type: entity
parent: BaseSubdermalImplant
id: FreedomImplant
name: freedom implant
description: you can break these cuffs
noSpawn: true
components:
- type: SubdermalImplant
implantAction: ActivateFreedomImplant
#Nuclear Operative/Special Exclusive implants
- type: entity
parent: BaseSubdermalImplant
id: MicroBombImplant
name: micro bomb implant
description: mission failed, user blows up on death to prevent company equipment from being stolen
noSpawn: true
components:
- type: SubdermalImplant
permanent: true
implantAction: ActivateMicroBomb
- type: TriggerOnMobstateChange
mobState: Dead
- type: TriggerImplantAction
- type: ExplodeOnTrigger
- type: GibOnTrigger
deleteItems: true
- type: Explosive
explosionType: MicroBomb
totalIntensity: 120
intensitySlope: 5
maxIntensity: 30
canCreateVacuum: false
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- MicroBomb
- type: entity
parent: BaseSubdermalImplant
id: MacroBombImplant
name: macro bomb implant
description: a large explosion packed into a small implant, be warned as this could be dangerous for your teammates
noSpawn: true
components:
- type: SubdermalImplant
permanent: true
- type: TriggerOnMobstateChange #Chains with OnUseTimerTrigger
mobState: Dead
- type: OnUseTimerTrigger
delay: 5
initialBeepDelay: 0
beepSound: /Audio/Machines/Nuke/general_beep.ogg
- type: ExplodeOnTrigger
- type: GibOnTrigger
deleteItems: true
- type: Explosive
explosionType: Default
totalIntensity: 4000
intensitySlope: 5
maxIntensity: 50
canCreateVacuum: true
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- MacroBomb

View File

@@ -70,9 +70,9 @@
key: enum.StoreUiKey.Key
- type: Store
preset: StorePresetUplink
balance:
balance:
Telecrystal: 0
- type: entity
parent: BaseUplinkRadio
id: BaseUplinkRadio20TC
@@ -82,6 +82,9 @@
preset: StorePresetUplink
balance:
Telecrystal: 20
- type: Tag
tags:
- TraitorUplink
- type: entity
parent: BaseUplinkRadio
@@ -92,6 +95,9 @@
preset: StorePresetUplink
balance:
Telecrystal: 25
- type: Tag
tags:
- TraitorUplink
#this uplink MUST be used for nukeops, as it has the tag for filtering the listing.
- type: entity

View File

@@ -185,6 +185,7 @@
- CryostasisBeaker
- Dropper
- Syringe
- Implanter
- PillCanister
- ChemistryEmptyBottle01
- Drone
@@ -406,6 +407,7 @@
- CryostasisBeaker
- Dropper
- Syringe
- Implanter
- PillCanister
- BodyBag
- ChemistryEmptyBottle01

View File

@@ -107,3 +107,12 @@
materials:
Cloth: 20
Plastic: 20
- type: latheRecipe
id: Implanter
icon: Objects/Specific/Chemistry/syringe.rsi/syringe_base0.png
result: Implanter
completetime: 1
materials:
Glass: 500
Steel: 500

View File

@@ -18,6 +18,8 @@
Piercing: 4
groups:
Burn: 3
- !type:AddImplantSpecial
implants: [ SadTromboneImplant ]
- type: startingGear
id: ClownGear

View File

@@ -0,0 +1,4 @@
- type: soundCollection
id: SadTrombone
files:
- /Audio/Effects/sadtrombone.ogg

View File

@@ -29,6 +29,22 @@
texturePath: /Textures/Effects/fire.rsi
fireStates: 3
- type: explosion
id: MicroBomb
damagePerIntensity:
types:
Heat: 6
Blunt: 6
Piercing: 6
Structural: 20
tileBreakChance: [ 0, 0.5, 1 ]
tileBreakIntensity: [ 1, 10, 15 ]
tileBreakRerollReduction: 30
intensityPerState: 20
lightColor: Orange
texturePath: /Textures/Effects/fire.rsi
fireStates: 6
- type: explosion
id: Radioactive
damagePerIntensity:
@@ -39,7 +55,7 @@
Piercing: 3
lightColor: Green
fireColor: Green
texturePath: /Textures/Effects/fire_greyscale.rsi
texturePath: /Textures/Effects/fire_greyscale.rsi
fireStates: 3
- type: explosion
@@ -47,10 +63,10 @@
damagePerIntensity:
types:
Cold: 5
Blunt: 2
Blunt: 2
tileBreakChance: [0]
tileBreakIntensity: [0]
lightColor: Blue
fireColor: Blue
texturePath: /Textures/Effects/fire_greyscale.rsi
texturePath: /Textures/Effects/fire_greyscale.rsi
fireStates: 3

View File

@@ -380,6 +380,9 @@
- type: Tag
id: NoSpinOnThrow
- type: Tag
id: TraitorUplink
- type: Tag
id: NoBlockAnchoring
@@ -395,6 +398,12 @@
- type: Tag
id: Payload # for grenade/bomb crafting
- type: Tag
id: MacroBomb
- type: Tag
id: MicroBomb
- type: Tag
id: PaintableAirlock
@@ -582,3 +591,5 @@
- type: Tag
id: Write
- type: Tag
id: SubdermalImplant

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Implant icons taken from Citadel Station at commit https://github.com/Citadel-Station-13/Citadel-Station-13/commit/a2f6a7c20763da3d2f3cfb982e9ccd7922df6162",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "freedom"
},
{
"name": "explosive"
}
]
}