diff --git a/Content.Client/HotPotato/HotPotatoSystem.cs b/Content.Client/HotPotato/HotPotatoSystem.cs new file mode 100644 index 0000000000..8e9c0f9e19 --- /dev/null +++ b/Content.Client/HotPotato/HotPotatoSystem.cs @@ -0,0 +1,26 @@ +using Content.Shared.HotPotato; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +public sealed class HotPotatoSystem : SharedHotPotatoSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_timing.IsFirstTimePredicted) + return; + + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var comp)) + { + if (_timing.CurTime < comp.TargetTime) + continue; + comp.TargetTime = _timing.CurTime + TimeSpan.FromSeconds(comp.EffectCooldown); + Spawn("HotPotatoEffect", Transform(uid).MapPosition.Offset(_random.NextVector2(0.25f))); + } + } +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 10539dd860..6e4360b5f8 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -36,6 +36,12 @@ namespace Content.Server.Explosion.EntitySystems } } + /// + /// Raised when timer trigger becomes active. + /// + [ByRefEvent] + public readonly record struct ActiveTimerTriggerEvent(EntityUid Triggered, EntityUid? User); + [UsedImplicitly] public sealed partial class TriggerSystem : EntitySystem { @@ -184,6 +190,9 @@ namespace Content.Server.Explosion.EntitySystems active.BeepInterval = beepInterval; active.TimeUntilBeep = initialBeepDelay == null ? active.BeepInterval : initialBeepDelay.Value; + var ev = new ActiveTimerTriggerEvent(uid, user); + RaiseLocalEvent(uid, ref ev); + if (TryComp(uid, out var appearance)) _appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Primed, appearance); } diff --git a/Content.Server/HotPotato/HotPotatoSystem.cs b/Content.Server/HotPotato/HotPotatoSystem.cs new file mode 100644 index 0000000000..79be700629 --- /dev/null +++ b/Content.Server/HotPotato/HotPotatoSystem.cs @@ -0,0 +1,55 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.HotPotato; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee.Events; + +namespace Content.Server.HotPotato; + +public sealed class HotPotatoSystem : SharedHotPotatoSystem +{ + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnActiveTimer); + SubscribeLocalEvent(OnMeleeHit); + } + + private void OnActiveTimer(EntityUid uid, HotPotatoComponent comp, ref ActiveTimerTriggerEvent args) + { + EnsureComp(uid); + comp.CanTransfer = false; + Dirty(comp); + } + + private void OnMeleeHit(EntityUid uid, HotPotatoComponent comp, MeleeHitEvent args) + { + if (!HasComp(uid)) + return; + + comp.CanTransfer = true; + foreach (var hitEntity in args.HitEntities) + { + if (!TryComp(hitEntity, out var hands)) + continue; + + if (!_hands.IsHolding(hitEntity, uid, out _, hands) && _hands.TryForcePickupAnyHand(hitEntity, uid, handsComp: hands)) + { + _popup.PopupEntity(Loc.GetString("hot-potato-passed", + ("from", args.User), ("to", hitEntity)), uid, PopupType.Medium); + break; + } + + _popup.PopupEntity(Loc.GetString("hot-potato-failed", + ("to", hitEntity)), uid, PopupType.Medium); + + break; + } + comp.CanTransfer = false; + Dirty(comp); + } +} diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs index 9732d3c215..846d67849c 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Pickup.cs @@ -104,6 +104,29 @@ public abstract partial class SharedHandsSystem : EntitySystem return true; } + /// + /// Tries to pick up an entity into any hand, forcing to drop an item if there are no free hands + /// By default it does check if it's possible to drop items + /// + public bool TryForcePickupAnyHand(EntityUid uid, EntityUid entity, bool checkActionBlocker = true, HandsComponent? handsComp = null, ItemComponent? item = null) + { + if (!Resolve(uid, ref handsComp, false)) + return false; + + if (TryPickupAnyHand(uid, entity, checkActionBlocker: checkActionBlocker, handsComp: handsComp)) + return true; + + foreach (var hand in handsComp.Hands.Values) + { + if (TryDrop(uid, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp) && + TryPickup(uid, entity, hand, checkActionBlocker: checkActionBlocker, handsComp: handsComp)) + { + return true; + } + } + return false; + } + public bool CanPickupAnyHand(EntityUid uid, EntityUid entity, bool checkActionBlocker = true, HandsComponent? handsComp = null, ItemComponent? item = null) { if (!Resolve(uid, ref handsComp, false)) diff --git a/Content.Shared/HotPotato/ActiveHotPotatoComponent.cs b/Content.Shared/HotPotato/ActiveHotPotatoComponent.cs new file mode 100644 index 0000000000..3a2a343783 --- /dev/null +++ b/Content.Shared/HotPotato/ActiveHotPotatoComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.HotPotato; + +/// +/// Added to an activated hot potato. Controls hot potato transfer on server / effect spawning on client. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedHotPotatoSystem))] +public sealed class ActiveHotPotatoComponent : Component +{ + /// + /// Hot potato effect spawn cooldown in seconds + /// + [DataField("effectCooldown"), ViewVariables(VVAccess.ReadWrite)] + public float EffectCooldown = 0.3f; + + /// + /// Moment in time next effect will be spawned + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan TargetTime = TimeSpan.Zero; +} diff --git a/Content.Shared/HotPotato/HotPotatoComponent.cs b/Content.Shared/HotPotato/HotPotatoComponent.cs new file mode 100644 index 0000000000..2d02e10626 --- /dev/null +++ b/Content.Shared/HotPotato/HotPotatoComponent.cs @@ -0,0 +1,20 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.HotPotato; + +/// +/// Similar to +/// except entities with this component can be removed in specific case: +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +[Access(typeof(SharedHotPotatoSystem))] +public sealed partial class HotPotatoComponent : Component +{ + /// + /// If set to true entity can be removed by hitting entities if they have hands + /// + [DataField("canTransfer"), ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public bool CanTransfer = true; +} diff --git a/Content.Shared/HotPotato/SharedHotPotatoSystem.cs b/Content.Shared/HotPotato/SharedHotPotatoSystem.cs new file mode 100644 index 0000000000..cd7a5d6da5 --- /dev/null +++ b/Content.Shared/HotPotato/SharedHotPotatoSystem.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Containers; + +namespace Content.Shared.HotPotato; + +public abstract class SharedHotPotatoSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRemoveAttempt); + } + + private void OnRemoveAttempt(EntityUid uid, HotPotatoComponent comp, ContainerGettingRemovedAttemptEvent args) + { + if (!comp.CanTransfer) + args.Cancel(); + } +} diff --git a/Resources/Locale/en-US/hot-potato/hot-potato.ftl b/Resources/Locale/en-US/hot-potato/hot-potato.ftl new file mode 100644 index 0000000000..76a691287c --- /dev/null +++ b/Resources/Locale/en-US/hot-potato/hot-potato.ftl @@ -0,0 +1,2 @@ +hot-potato-passed = {$from} passed hot potato to {$to}! +hot-potato-failed = Can't pass the potato to {$to}! diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 0c5225b619..0341c9a387 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -157,6 +157,9 @@ uplink-revolver-cap-gun-fake-desc = Fool your enemy! It can use both cap and mag uplink-banana-peel-explosive-name = Explosive Banana Peel uplink-banana-peel-explosive-desc = They will burst into laughter when they slip on it! +uplink-hot-potato-name = Hot Potato +uplink-hot-potato-desc = Once activated, this time bomb can't be dropped - only passed to someone else! + # Armor uplink-chameleon-name = Chameleon Kit uplink-chameleon-desc = A backpack full of items that contain chameleon technology allowing you to disguise as pretty much anything on the station, and more! diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index d42987c09e..b73174416c 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -620,6 +620,23 @@ whitelist: - Clown +- type: listing + id: uplinkHotPotato + name: uplink-hot-potato-name + description: uplink-hot-potato-desc + productEntity: HotPotato + cost: + Telecrystal: 4 + categories: + - UplinkJob + conditions: + - !type:BuyerJobCondition + whitelist: + - Chef + - Botanist + - Clown + - Mime + # Armor - type: listing diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml new file mode 100644 index 0000000000..c7c7f272fc --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml @@ -0,0 +1,55 @@ +- type: entity + name: hot potato + description: Once activated, this time bomb can't be dropped - only passed to someone else! + parent: BaseItem + id: HotPotato + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/hot_potato.rsi + state: icon + netsync: false + - type: Item + sprite: Objects/Weapons/Bombs/hot_potato.rsi + size: 5 + - type: MeleeWeapon + damage: + types: + Blunt: 5 + - type: OnUseTimerTrigger + delay: 180 + beepSound: /Audio/Machines/Nuke/general_beep.ogg + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Default + maxIntensity: 8 + intensitySlope: 5 + totalIntensity: 20 + canCreateVacuum: false + - type: DeleteOnTrigger + - type: HotPotato + - type: Appearance + visuals: + - type: GenericEnumVisualizer + key: enum.Trigger.TriggerVisuals.VisualState + states: + enum.Trigger.TriggerVisualState.Primed: activated + enum.Trigger.TriggerVisualState.Unprimed: complete + +- type: entity + id: HotPotatoEffect + noSpawn: true + components: + - type: TimedDespawn + lifetime: 0.6 + - type: Sprite + netsync: false + noRot: true + drawdepth: Effects + sprite: Effects/chemsmoke.rsi + state: chemsmoke + scale: "0.15, 0.15" + - type: EffectVisuals + - type: Tag + tags: + - HideContextMenu + - type: AnimationPlayer diff --git a/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/activated.png b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/activated.png new file mode 100644 index 0000000000..07785ce832 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/activated.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/icon.png b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/icon.png new file mode 100644 index 0000000000..426106d046 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/meta.json b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/meta.json new file mode 100644 index 0000000000..664e16d42c --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Bombs/hot_potato.rsi/meta.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/vgstation-coders/vgstation13/commit/1dbcf389b0ec6b2c51b002df5fef8dd1519f8068", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "activated", + "delays": [ + [ + 0.1, + 0.1 + ] + ] + } + ] +} \ No newline at end of file