diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index f4fcac686c..10778032cc 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -201,6 +201,7 @@ namespace Content.Client.Entry "GasAnalyzable", "GasCanister", "GasPort", + "Sticky", "GasPortable", "AtmosPipeColor", "AtmosUnsafeUnanchor", diff --git a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs new file mode 100644 index 0000000000..fdf80ff41a --- /dev/null +++ b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Sticky.Components; +using Robust.Client.GameObjects; + +namespace Content.Client.Sticky.Visualizers; + +public sealed class StickyVisualizerSystem : VisualizerSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + } + + private void OnInit(EntityUid uid, StickyVisualizerComponent component, ComponentInit args) + { + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + component.DefaultDrawDepth = sprite.DrawDepth; + } + + protected override void OnAppearanceChange(EntityUid uid, StickyVisualizerComponent component, ref AppearanceChangeEvent args) + { + base.OnAppearanceChange(uid, component, ref args); + + if (!TryComp(uid, out SpriteComponent? sprite)) + return; + + if (!args.Component.TryGetData(StickyVisuals.IsStuck, out bool isStuck)) + return; + + var drawDepth = isStuck ? component.StuckDrawDepth : component.DefaultDrawDepth; + sprite.DrawDepth = drawDepth; + + } +} diff --git a/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs b/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs index a76c876421..d8f7fba643 100644 --- a/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs +++ b/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs @@ -32,5 +32,12 @@ namespace Content.Server.Explosion.Components [DataField("beepParams")] public AudioParams BeepParams = AudioParams.Default.WithVolume(-2f); + + /// + /// Should timer be started when it was stuck to another entity. + /// Used for C4 charges and similar behaviour. + /// + [DataField("startOnStick")] + public bool StartOnStick; } } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index 38389f856c..08b454d8f1 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -1,4 +1,5 @@ using Content.Server.Explosion.Components; +using Content.Server.Sticky.Events; using Content.Shared.Examine; using Content.Shared.Popups; using Content.Shared.Interaction.Events; @@ -16,6 +17,22 @@ public sealed partial class TriggerSystem SubscribeLocalEvent(OnTimerUse); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent>(OnGetAltVerbs); + SubscribeLocalEvent(OnStuck); + } + + private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, EntityStuckEvent args) + { + if (!component.StartOnStick) + return; + + HandleTimerTrigger( + uid, + args.User, + component.Delay, + component.BeepInterval, + component.InitialBeepDelay, + component.BeepSound, + component.BeepParams); } private void OnExamined(EntityUid uid, OnUseTimerTriggerComponent component, ExaminedEvent args) diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 6b53e91e37..aab92a3e3b 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Doors.Systems; using Content.Server.Explosion.Components; using Content.Server.Flash; using Content.Server.Flash.Components; +using Content.Server.Sticky.Events; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Physics; diff --git a/Content.Server/Sticky/Components/StickyComponent.cs b/Content.Server/Sticky/Components/StickyComponent.cs new file mode 100644 index 0000000000..3d71934f90 --- /dev/null +++ b/Content.Server/Sticky/Components/StickyComponent.cs @@ -0,0 +1,76 @@ +using Content.Shared.Whitelist; + +namespace Content.Server.Sticky.Components; + +/// +/// Items that can be stick to other structures or entities. +/// For example paper stickers or C4 charges. +/// +[RegisterComponent] +public sealed class StickyComponent : Component +{ + /// + /// What target entities are valid to be surface for sticky entity. + /// + [DataField("whitelist")] + [ViewVariables(VVAccess.ReadWrite)] + public EntityWhitelist? Whitelist; + + /// + /// How much time does it take to stick entity to target. + /// If zero will stick entity immediately. + /// + [DataField("stickDelay")] + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan StickDelay = TimeSpan.Zero; + + /// + /// Whether users can unstick item when it was stuck to surface. + /// + [DataField("canUnstick")] + [ViewVariables(VVAccess.ReadWrite)] + public bool CanUnstick = true; + + /// + /// How much time does it take to unstick entity. + /// If zero will unstick entity immediately. + /// + [DataField("unstickDelay")] + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan UnstickDelay = TimeSpan.Zero; + + /// + /// Popup message shown when player started sticking entity to another entity. + /// + [DataField("stickPopupStart")] + [ViewVariables(VVAccess.ReadWrite)] + public string? StickPopupStart; + + /// + /// Popup message shown when player successfully stuck entity. + /// + [DataField("stickPopupSuccess")] + [ViewVariables(VVAccess.ReadWrite)] + public string? StickPopupSuccess; + + /// + /// Popup message shown when player started unsticking entity from another entity. + /// + [DataField("unstickPopupStart")] + [ViewVariables(VVAccess.ReadWrite)] + public string? UnstickPopupStart; + + /// + /// Popup message shown when player successfully unstuck entity. + /// + [DataField("unstickPopupSuccess")] + [ViewVariables(VVAccess.ReadWrite)] + public string? UnstickPopupSuccess; + + /// + /// Entity that is used as surface for sticky entity. + /// Null if entity doesn't stuck to anything. + /// + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? StuckTo; +} diff --git a/Content.Server/Sticky/Events/EntityStuckEvent.cs b/Content.Server/Sticky/Events/EntityStuckEvent.cs new file mode 100644 index 0000000000..b924436489 --- /dev/null +++ b/Content.Server/Sticky/Events/EntityStuckEvent.cs @@ -0,0 +1,45 @@ +namespace Content.Server.Sticky.Events; + +/// +/// Risen on sticky entity when it was stuck to other entity. +/// +public sealed class EntityStuckEvent : EntityEventArgs +{ + /// + /// Entity that was used as a surface for sticky object. + /// + public readonly EntityUid Target; + + /// + /// Entity that stuck sticky object on target. + /// + public readonly EntityUid User; + + public EntityStuckEvent(EntityUid target, EntityUid user) + { + Target = target; + User = user; + } +} + +/// +/// Risen on sticky entity when it was unstuck from other entity. +/// +public sealed class EntityUnstuckEvent : EntityEventArgs +{ + /// + /// Entity that was used as a surface for sticky object. + /// + public readonly EntityUid Target; + + /// + /// Entity that unstuck sticky object on target. + /// + public readonly EntityUid User; + + public EntityUnstuckEvent(EntityUid target, EntityUid user) + { + Target = target; + User = user; + } +} diff --git a/Content.Server/Sticky/Systems/StickySystem.cs b/Content.Server/Sticky/Systems/StickySystem.cs new file mode 100644 index 0000000000..4634808877 --- /dev/null +++ b/Content.Server/Sticky/Systems/StickySystem.cs @@ -0,0 +1,231 @@ +using Content.Server.DoAfter; +using Content.Server.Popups; +using Content.Server.Sticky.Components; +using Content.Server.Sticky.Events; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Sticky.Components; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Player; + +namespace Content.Server.Sticky.Systems; + +public sealed class StickySystem : EntitySystem +{ + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + + private const string StickerSlotId = "stickers_container"; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStickSuccessful); + SubscribeLocalEvent(OnUnstickSuccessful); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent>(AddUnstickVerb); + } + + private void OnAfterInteract(EntityUid uid, StickyComponent component, AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target == null) + return; + + // try stick object to a clicked target entity + args.Handled = StartSticking(uid, args.User, args.Target.Value, component); + } + + private void AddUnstickVerb(EntityUid uid, StickyComponent component, GetVerbsEvent args) + { + if (component.StuckTo == null || !component.CanUnstick) + return; + + args.Verbs.Add(new Verb + { + Text = Loc.GetString("comp-sticky-unstick-verb-text"), + IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png", + Act = () => StartUnsticking(uid, args.User, component) + }); + } + + private bool StartSticking(EntityUid uid, EntityUid user, EntityUid target, StickyComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + if (component.Whitelist != null && !component.Whitelist.IsValid(target)) + return false; + + // check if delay is not zero to start do after + var delay = (float) component.StickDelay.TotalSeconds; + if (delay > 0) + { + // show message to user + if (component.StickPopupStart != null) + { + var msg = Loc.GetString(component.StickPopupStart); + _popupSystem.PopupEntity(msg, user, Filter.Entities(user)); + } + + // start sticking object to target + _doAfterSystem.DoAfter(new DoAfterEventArgs(user, delay, target: target) + { + BroadcastFinishedEvent = new StickSuccessfulEvent(uid, user, target), + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true + }); + } + else + { + // if delay is zero - stick entity immediately + StickToEntity(uid, target, user, component); + } + + return true; + } + + private void OnStickSuccessful(StickSuccessfulEvent ev) + { + // check if entity still has sticky component + if (!TryComp(ev.Uid, out StickyComponent? component)) + return; + + StickToEntity(ev.Uid, ev.Target, ev.User, component); + } + + private void StartUnsticking(EntityUid uid, EntityUid user, StickyComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var delay = (float) component.UnstickDelay.TotalSeconds; + if (delay > 0) + { + // show message to user + if (component.UnstickPopupStart != null) + { + var msg = Loc.GetString(component.UnstickPopupStart); + _popupSystem.PopupEntity(msg, user, Filter.Entities(user)); + } + + // start unsticking object + _doAfterSystem.DoAfter(new DoAfterEventArgs(user, delay, target: uid) + { + BroadcastFinishedEvent = new UnstickSuccessfulEvent(uid, user), + BreakOnStun = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true + }); + } + else + { + // if delay is zero - unstick entity immediately + UnstickFromEntity(uid, user, component); + } + + return; + } + + private void OnUnstickSuccessful(UnstickSuccessfulEvent ev) + { + // check if entity still has sticky component + if (!TryComp(ev.Uid, out StickyComponent? component)) + return; + + UnstickFromEntity(ev.Uid, ev.User, component); + } + + public void StickToEntity(EntityUid uid, EntityUid target, EntityUid user, StickyComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + // add container to entity and insert sticker into it + var container = _containerSystem.EnsureContainer(target, StickerSlotId); + container.ShowContents = true; + if (!container.Insert(uid)) + return; + + // show message to user + if (component.StickPopupSuccess != null) + { + var msg = Loc.GetString(component.StickPopupSuccess); + _popupSystem.PopupEntity(msg, user, Filter.Entities(user)); + } + + // send information to appearance that entity is stuck + if (TryComp(uid, out AppearanceComponent? appearance)) + { + appearance.SetData(StickyVisuals.IsStuck, true); + } + + component.StuckTo = target; + RaiseLocalEvent(uid, new EntityStuckEvent(target, user)); + } + + public void UnstickFromEntity(EntityUid uid, EntityUid user, StickyComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + if (component.StuckTo == null) + return; + + // try to remove sticky item from target container + var target = component.StuckTo.Value; + if (!_containerSystem.TryGetContainer(target, StickerSlotId, out var container) || !container.Remove(uid)) + return; + // delete container if it's now empty + if (container.ContainedEntities.Count == 0) + container.Shutdown(); + + // try place dropped entity into user hands + _handsSystem.PickupOrDrop(user, uid); + + // send information to appearance that entity isn't stuck + if (TryComp(uid, out AppearanceComponent? appearance)) + { + appearance.SetData(StickyVisuals.IsStuck, false); + } + + // show message to user + if (component.UnstickPopupSuccess != null) + { + var msg = Loc.GetString(component.UnstickPopupSuccess); + _popupSystem.PopupEntity(msg, user, Filter.Entities(user)); + } + + component.StuckTo = null; + RaiseLocalEvent(uid, new EntityUnstuckEvent(target, user)); + } + + private sealed class StickSuccessfulEvent : EntityEventArgs + { + public readonly EntityUid Uid; + public readonly EntityUid User; + public readonly EntityUid Target; + + public StickSuccessfulEvent(EntityUid uid, EntityUid user, EntityUid target) + { + Uid = uid; + User = user; + Target = target; + } + } + + private sealed class UnstickSuccessfulEvent : EntityEventArgs + { + public readonly EntityUid Uid; + public readonly EntityUid User; + + public UnstickSuccessfulEvent(EntityUid uid, EntityUid user) + { + Uid = uid; + User = user; + } + } +} diff --git a/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs b/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs new file mode 100644 index 0000000000..9bb7e621ab --- /dev/null +++ b/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs @@ -0,0 +1,27 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Sticky.Components; +using DrawDepth; + +[RegisterComponent] +public sealed class StickyVisualizerComponent : Component +{ + /// + /// What sprite draw depth set when entity stuck. + /// + [DataField("stuckDrawDepth")] + [ViewVariables(VVAccess.ReadWrite)] + public int StuckDrawDepth = (int) DrawDepth.Overdoors; + + /// + /// What sprite draw depth set when entity unstuck. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int DefaultDrawDepth; +} + +[Serializable, NetSerializable] +public enum StickyVisuals : byte +{ + IsStuck +} diff --git a/Resources/Locale/en-US/sticky/sticky-component.ftl b/Resources/Locale/en-US/sticky/sticky-component.ftl new file mode 100644 index 0000000000..9464a602a7 --- /dev/null +++ b/Resources/Locale/en-US/sticky/sticky-component.ftl @@ -0,0 +1,9 @@ +# Bomb planting strings + +comp-sticky-start-stick-bomb = You start planting the bomb... +comp-sticky-success-stick-bomb = You planted the bomb +comp-sticky-start-unstick-bomb = You start carefully removing the bomb... +comp-sticky-success-unstick-bomb = You removed the bomb + +# General strings +comp-sticky-unstick-verb-text = Unstick diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml index 54902f41c3..34020f9c29 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml @@ -169,6 +169,17 @@ - id: PlushieNuke - id: PlushieLizard +- type: entity + parent: ClothingBackpackDuffelSyndicate + id: ClothingBackpackDuffelSyndicateC4tBundle + name: syndicate C-4 bundle + description: "Contains a lot of C-4 charges." + components: + - type: StorageFill + contents: + - id: C4 + amount: 8 + - type: entity parent: ClothingBackpackDuffelSyndicate id: ClothingBackpackDuffelSyndicateHardsuitBundle diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 3911d39500..fd24c3a990 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -90,17 +90,21 @@ itemId: MobGrenadePenguin price: 6 -#- type: uplinkListing -# id: UplinkExplosiveC4 -# category: Weapons -# itemId: ExplosiveC4 -# price: 5 +- type: uplinkListing + id: UplinkC4 + category: Explosives + itemId: C4 + price: 2 + description: > + C-4 is plastic explosive of the common variety Composition C. You can use it to breach walls, airlocks or sabotage equipment. + It can be attached to almost all objects and has a modifiable timer with a minimum setting of 10 seconds. -#- type: uplinkListing -# id: UplinkDuffelExplosiveC4 -# category: Weapons -# itemId: DuffelExplosiveC4 -# price: 15 +- type: uplinkListing + id: UplinkC4Bundle + category: Explosives + itemId: ClothingBackpackDuffelSyndicateC4tBundle + price: 12 # 25% off + description: Because sometimes quantity is quality. Contains 8 C-4 plastic explosives. # Ammo diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/plastic.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/plastic.yml new file mode 100644 index 0000000000..c68498cab8 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/plastic.yml @@ -0,0 +1,50 @@ +- type: entity + name: composition C-4 + description: Used to put holes in specific areas without too much extra hole. A saboteur's favorite. + parent: BaseItem + id: C4 + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/c4.rsi + state: icon + - type: Item + sprite: Objects/Weapons/Bombs/c4.rsi + size: 10 + - type: OnUseTimerTrigger + delay: 10 + delayOptions: [10, 30, 60, 120, 300] + initialBeepDelay: 0 + beepSound: /Audio/Machines/Nuke/general_beep.ogg + startOnStick: true + - type: Sticky + stickDelay: 5 + unstickDelay: 5 + stickPopupStart: comp-sticky-start-stick-bomb + stickPopupSuccess: comp-sticky-success-stick-bomb + unstickPopupStart: comp-sticky-start-unstick-bomb + unstickPopupSuccess: comp-sticky-success-unstick-bomb + - type: Explosive # Powerful explosion in a very small radius. Doesn't break underplating. + explosionType: Default + maxIntensity: 300 + intensitySlope: 100 + totalIntensity: 300 + canCreateVacuum: false + - type: ExplodeOnTrigger + - type: Damageable + damageContainer: Inorganic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 10 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: StickyVisualizer + - type: Appearance + visuals: + - type: GenericEnumVisualizer + key: enum.Trigger.TriggerVisuals.VisualState + states: + enum.Trigger.TriggerVisualState.Primed: primed + enum.Trigger.TriggerVisualState.Unprimed: complete diff --git a/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/icon.png b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/icon.png new file mode 100644 index 0000000000..07c8b69565 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-left.png new file mode 100644 index 0000000000..82921b2236 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-right.png new file mode 100644 index 0000000000..6d9a1544ab Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/meta.json b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/meta.json new file mode 100644 index 0000000000..3e3124c037 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/meta.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/b13d244d761a07e200a9a41730bd446e776020d5", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "primed", + "delays": [ + [ + 0.1, + 0.1 + ] + ] + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/primed.png b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/primed.png new file mode 100644 index 0000000000..198882fec3 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/c4.rsi/primed.png differ