--- /dev/null
+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<ActiveHotPotatoComponent>();
+ 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)));
+ }
+ }
+}
}
}
+ /// <summary>
+ /// Raised when timer trigger becomes active.
+ /// </summary>
+ [ByRefEvent]
+ public readonly record struct ActiveTimerTriggerEvent(EntityUid Triggered, EntityUid? User);
+
[UsedImplicitly]
public sealed partial class TriggerSystem : EntitySystem
{
active.BeepInterval = beepInterval;
active.TimeUntilBeep = initialBeepDelay == null ? active.BeepInterval : initialBeepDelay.Value;
+ var ev = new ActiveTimerTriggerEvent(uid, user);
+ RaiseLocalEvent(uid, ref ev);
+
if (TryComp<AppearanceComponent>(uid, out var appearance))
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Primed, appearance);
}
--- /dev/null
+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<HotPotatoComponent, ActiveTimerTriggerEvent>(OnActiveTimer);
+ SubscribeLocalEvent<HotPotatoComponent, MeleeHitEvent>(OnMeleeHit);
+ }
+
+ private void OnActiveTimer(EntityUid uid, HotPotatoComponent comp, ref ActiveTimerTriggerEvent args)
+ {
+ EnsureComp<ActiveHotPotatoComponent>(uid);
+ comp.CanTransfer = false;
+ Dirty(comp);
+ }
+
+ private void OnMeleeHit(EntityUid uid, HotPotatoComponent comp, MeleeHitEvent args)
+ {
+ if (!HasComp<ActiveHotPotatoComponent>(uid))
+ return;
+
+ comp.CanTransfer = true;
+ foreach (var hitEntity in args.HitEntities)
+ {
+ if (!TryComp<HandsComponent>(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);
+ }
+}
return true;
}
+ /// <summary>
+ /// 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
+ /// </summary>
+ 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))
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.HotPotato;
+
+/// <summary>
+/// Added to an activated hot potato. Controls hot potato transfer on server / effect spawning on client.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedHotPotatoSystem))]
+public sealed class ActiveHotPotatoComponent : Component
+{
+ /// <summary>
+ /// Hot potato effect spawn cooldown in seconds
+ /// </summary>
+ [DataField("effectCooldown"), ViewVariables(VVAccess.ReadWrite)]
+ public float EffectCooldown = 0.3f;
+
+ /// <summary>
+ /// Moment in time next effect will be spawned
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan TargetTime = TimeSpan.Zero;
+}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.HotPotato;
+
+/// <summary>
+/// Similar to <see cref="Content.Shared.Interaction.Components.UnremoveableComponent"/>
+/// except entities with this component can be removed in specific case: <see cref="CanTransfer"/>
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState]
+[Access(typeof(SharedHotPotatoSystem))]
+public sealed partial class HotPotatoComponent : Component
+{
+ /// <summary>
+ /// If set to true entity can be removed by hitting entities if they have hands
+ /// </summary>
+ [DataField("canTransfer"), ViewVariables(VVAccess.ReadWrite)]
+ [AutoNetworkedField]
+ public bool CanTransfer = true;
+}
--- /dev/null
+using Robust.Shared.Containers;
+
+namespace Content.Shared.HotPotato;
+
+public abstract class SharedHotPotatoSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<HotPotatoComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
+ }
+
+ private void OnRemoveAttempt(EntityUid uid, HotPotatoComponent comp, ContainerGettingRemovedAttemptEvent args)
+ {
+ if (!comp.CanTransfer)
+ args.Cancel();
+ }
+}
--- /dev/null
+hot-potato-passed = {$from} passed hot potato to {$to}!
+hot-potato-failed = Can't pass the potato to {$to}!
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!
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
--- /dev/null
+- 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
--- /dev/null
+{
+ "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