diff --git a/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs b/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs
new file mode 100644
index 0000000000..c0d267f0d2
--- /dev/null
+++ b/Content.Server/Destructible/Thresholds/Behaviors/EjectVendorItems.cs
@@ -0,0 +1,47 @@
+using Content.Server.VendingMachines;
+using Content.Shared.Throwing;
+using Robust.Shared.Random;
+
+namespace Content.Server.Destructible.Thresholds.Behaviors
+{
+ ///
+ /// Throws out a specific amount of random items from a vendor
+ ///
+ [Serializable]
+ [DataDefinition]
+ public sealed class EjectVendorItems : IThresholdBehavior
+ {
+ ///
+ /// The percent amount of the total inventory that will be ejected.
+ ///
+ [DataField("percent", required: true)]
+ public float Percent = 0.25f;
+
+ ///
+ /// The maximum amount of vendor items it can eject
+ /// useful for high-inventory vendors
+ ///
+ [DataField("max")]
+ public int Max = 3;
+
+ public void Execute(EntityUid owner, DestructibleSystem system)
+ {
+ if (!system.EntityManager.TryGetComponent(owner, out var vendingcomp) ||
+ !system.EntityManager.TryGetComponent(owner, out var xform))
+ return;
+
+ var throwingsys = system.EntityManager.EntitySysManager.GetEntitySystem();
+ var totalItems = vendingcomp.AllInventory.Count;
+
+ var toEject = Math.Min(totalItems * Percent, Max);
+ for (var i = 0; i < toEject; i++)
+ {
+ var entity = system.EntityManager.SpawnEntity(system.Random.PickAndTake(vendingcomp.AllInventory).ID, xform.Coordinates);
+
+ float range = vendingcomp.NonLimitedEjectRange;
+ Vector2 direction = new Vector2(system.Random.NextFloat(-range, range), system.Random.NextFloat(-range, range));
+ throwingsys.TryThrow(entity, direction, vendingcomp.NonLimitedEjectForce);
+ }
+ }
+ }
+}
diff --git a/Content.Server/VendingMachines/VendingMachineComponent.cs b/Content.Server/VendingMachines/VendingMachineComponent.cs
index 244bc7fd4b..2db45ad428 100644
--- a/Content.Server/VendingMachines/VendingMachineComponent.cs
+++ b/Content.Server/VendingMachines/VendingMachineComponent.cs
@@ -1,4 +1,5 @@
using Content.Server.UserInterface;
+using Content.Shared.Actions.ActionTypes;
using Content.Shared.Sound;
using Content.Shared.VendingMachines;
using Robust.Server.GameObjects;
@@ -26,6 +27,16 @@ namespace Content.Server.VendingMachines
[DataField("speedLimiter")]
public bool CanShoot = false;
+ ///
+ /// The chance that a vending machine will randomly dispense an item on hit.
+ /// Chance is 0 if null.
+ ///
+ [DataField("dispenseOnHitChance")]
+ public float? DispenseOnHitChance;
+
+ [DataField("dispenseOnHitThreshold")]
+ public float? DispenseOnHitThreshold;
+
[DataField("soundVend")]
// Grabbed from: https://github.com/discordia-space/CEV-Eris/blob/f702afa271136d093ddeb415423240a2ceb212f0/sound/machines/vending_drop.ogg
public SoundSpecifier SoundVend = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg");
@@ -34,6 +45,9 @@ namespace Content.Server.VendingMachines
// Yoinked from: https://github.com/discordia-space/CEV-Eris/blob/35bbad6764b14e15c03a816e3e89aa1751660ba9/sound/machines/Custom_deny.ogg
public SoundSpecifier SoundDeny = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
+ [DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? Action = "VendingThrow";
+
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(VendingMachineUiKey.Key);
public float NonLimitedEjectForce = 7.5f;
diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs
index a5d873182b..90b1e51f41 100644
--- a/Content.Server/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Server/VendingMachines/VendingMachineSystem.cs
@@ -4,6 +4,9 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
+using Content.Shared.Actions;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.Emag.Systems;
using Content.Shared.Throwing;
@@ -24,6 +27,7 @@ namespace Content.Server.VendingMachines
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _action = default!;
public override void Initialize()
{
@@ -34,6 +38,9 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent(OnInventoryEjectMessage);
SubscribeLocalEvent(OnBreak);
SubscribeLocalEvent(OnEmagged);
+ SubscribeLocalEvent(OnDamage);
+
+ SubscribeLocalEvent(OnSelfDispense);
}
private void OnComponentInit(EntityUid uid, VendingMachineComponent component, ComponentInit args)
@@ -45,6 +52,12 @@ namespace Content.Server.VendingMachines
TryUpdateVisualState(uid, null, component);
}
+ if (component.Action != null)
+ {
+ var action = new InstantAction(_prototypeManager.Index(component.Action));
+ _action.AddAction(uid, action, uid);
+ }
+
InitializeFromPrototype(uid, component);
}
@@ -91,6 +104,24 @@ namespace Content.Server.VendingMachines
component.Emagged = true;
args.Handled = true;
}
+
+ private void OnDamage(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
+ {
+ if (component.DispenseOnHitChance == null || args.DamageDelta == null)
+ return;
+
+ if (args.DamageDelta.Total >= component.DispenseOnHitThreshold && _random.Prob(component.DispenseOnHitChance.Value))
+ EjectRandom(uid, true, component);
+ }
+
+ private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ args.Handled = true;
+ EjectRandom(uid, true, component);
+ }
public void InitializeFromPrototype(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
diff --git a/Content.Shared/VendingMachines/SharedVendingMachineComponent.cs b/Content.Shared/VendingMachines/SharedVendingMachineComponent.cs
index a122c6d4de..6084ec0f76 100644
--- a/Content.Shared/VendingMachines/SharedVendingMachineComponent.cs
+++ b/Content.Shared/VendingMachines/SharedVendingMachineComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Actions;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -121,4 +122,6 @@ namespace Content.Shared.VendingMachines
{
StatusKey,
}
+
+ public sealed class VendingMachineSelfDispenseEvent : InstantActionEvent { };
}
diff --git a/Resources/Locale/en-US/vending-machines/vending-machine-component.ftl b/Resources/Locale/en-US/vending-machines/vending-machine-component.ftl
index 3cd6b38d66..0991982241 100644
--- a/Resources/Locale/en-US/vending-machines/vending-machine-component.ftl
+++ b/Resources/Locale/en-US/vending-machines/vending-machine-component.ftl
@@ -2,4 +2,9 @@
vending-machine-component-try-eject-invalid-item = Invalid item
vending-machine-component-try-eject-out-of-stock = Out of stock
-vending-machine-component-try-eject-access-denied = Access denied
\ No newline at end of file
+vending-machine-component-try-eject-access-denied = Access denied
+
+## VendingMachineSelfDespense Action
+
+vending-machine-action-name = Dispense Item
+vending-machine-action-description = Randomly dispense an item from your stock.
\ No newline at end of file
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index 0269fb0952..8c3d9ea9f6 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -48,3 +48,10 @@
userPopup: action-popup-combat
popupToggleSuffix: -disabling
event: !type:ToggleCombatActionEvent
+
+- type: instantAction
+ id: VendingThrow
+ name: vending-machine-action-name
+ description: vending-machine-action-description
+ useDelay: 30
+ event: !type:VendingMachineSelfDispenseEvent
diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
index d5460219d5..46b6f95ffa 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
@@ -37,6 +37,7 @@
behaviors:
- !type:DoActsBehavior
acts: ["Breakage"]
+ - !type:EjectVendorItems
- type: ActivatableUI
key: enum.VendingMachineUiKey.Key
- type: ActivatableUIRequiresPower
@@ -64,6 +65,7 @@
- type: ApcPowerReceiver
powerLoad: 200
priority: Low
+ - type: Actions
- type: SentienceTarget
flavorKind: mechanical
@@ -166,6 +168,8 @@
components:
- type: VendingMachine
pack: CigaretteMachineInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: CigaretteMachineAds
- type: Speech
@@ -232,6 +236,8 @@
components:
- type: VendingMachine
pack: HotDrinksMachineInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: HotDrinksMachineAds
- type: Speech
@@ -270,6 +276,8 @@
components:
- type: VendingMachine
pack: RobustSoftdrinksInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: RobustSoftdrinksAds
- type: Speech
@@ -338,6 +346,8 @@
components:
- type: VendingMachine
pack: DiscountDansInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: DiscountDansAds
- type: Speech
@@ -574,6 +584,8 @@
components:
- type: VendingMachine
pack: GetmoreChocolateCorpInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: GetmoreChocolateCorpAds
- type: Speech
@@ -608,6 +620,8 @@
components:
- type: VendingMachine
pack: BodaInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: BodaAds
- type: Speech
@@ -815,6 +829,8 @@
components:
- type: VendingMachine
pack: ChangInventory
+ dispenseOnHitChance: 0.25
+ dispenseOnHitThreshold: 2
- type: Advertise
pack: ChangAds
- type: Speech