VendingMachineSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Server.Cargo.Systems;
  4. using Content.Server.Emp;
  5. using Content.Server.Power.Components;
  6. using Content.Server.Power.EntitySystems;
  7. using Content.Shared.Damage;
  8. using Content.Shared.Destructible;
  9. using Content.Shared.DoAfter;
  10. using Content.Shared.Emp;
  11. using Content.Shared.Popups;
  12. using Content.Shared.Power;
  13. using Content.Shared.Throwing;
  14. using Content.Shared.UserInterface;
  15. using Content.Shared.VendingMachines;
  16. using Content.Shared.Wall;
  17. using Robust.Shared.Audio;
  18. using Robust.Shared.Prototypes;
  19. using Robust.Shared.Random;
  20. using Robust.Shared.Timing;
  21. namespace Content.Server.VendingMachines
  22. {
  23. public sealed class VendingMachineSystem : SharedVendingMachineSystem
  24. {
  25. [Dependency] private readonly IRobustRandom _random = default!;
  26. [Dependency] private readonly PricingSystem _pricing = default!;
  27. [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
  28. [Dependency] private readonly IGameTiming _timing = default!;
  29. private const float WallVendEjectDistanceFromWall = 1f;
  30. public override void Initialize()
  31. {
  32. base.Initialize();
  33. SubscribeLocalEvent<VendingMachineComponent, PowerChangedEvent>(OnPowerChanged);
  34. SubscribeLocalEvent<VendingMachineComponent, BreakageEventArgs>(OnBreak);
  35. SubscribeLocalEvent<VendingMachineComponent, DamageChangedEvent>(OnDamageChanged);
  36. SubscribeLocalEvent<VendingMachineComponent, PriceCalculationEvent>(OnVendingPrice);
  37. SubscribeLocalEvent<VendingMachineComponent, EmpPulseEvent>(OnEmpPulse);
  38. SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
  39. SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
  40. SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
  41. SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
  42. }
  43. private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
  44. {
  45. var price = 0.0;
  46. foreach (var entry in component.Inventory.Values)
  47. {
  48. if (!PrototypeManager.TryIndex<EntityPrototype>(entry.ID, out var proto))
  49. {
  50. Log.Error($"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending.");
  51. continue;
  52. }
  53. price += entry.Amount * _pricing.GetEstimatedPrice(proto);
  54. }
  55. args.Price += price;
  56. }
  57. protected override void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
  58. {
  59. base.OnMapInit(uid, component, args);
  60. if (HasComp<ApcPowerReceiverComponent>(uid))
  61. {
  62. TryUpdateVisualState((uid, component));
  63. }
  64. }
  65. private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args)
  66. {
  67. if (component.Broken)
  68. args.Cancel();
  69. }
  70. private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
  71. {
  72. TryUpdateVisualState((uid, component));
  73. }
  74. private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
  75. {
  76. vendComponent.Broken = true;
  77. TryUpdateVisualState((uid, vendComponent));
  78. }
  79. private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
  80. {
  81. if (!args.DamageIncreased && component.Broken)
  82. {
  83. component.Broken = false;
  84. TryUpdateVisualState((uid, component));
  85. return;
  86. }
  87. if (component.Broken || component.DispenseOnHitCoolingDown ||
  88. component.DispenseOnHitChance == null || args.DamageDelta == null)
  89. return;
  90. if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
  91. _random.Prob(component.DispenseOnHitChance.Value))
  92. {
  93. if (component.DispenseOnHitCooldown != null)
  94. {
  95. component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
  96. }
  97. EjectRandom(uid, throwItem: true, forceEject: true, component);
  98. }
  99. }
  100. private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args)
  101. {
  102. if (args.Handled)
  103. return;
  104. args.Handled = true;
  105. EjectRandom(uid, throwItem: true, forceEject: false, component);
  106. }
  107. private void OnDoAfter(EntityUid uid, VendingMachineComponent component, DoAfterEvent args)
  108. {
  109. if (args.Handled || args.Cancelled || args.Args.Used == null)
  110. return;
  111. if (!TryComp<VendingMachineRestockComponent>(args.Args.Used, out var restockComponent))
  112. {
  113. Log.Error($"{ToPrettyString(args.Args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.Args.Used.Value)} which did not have a VendingMachineRestockComponent.");
  114. return;
  115. }
  116. TryRestockInventory(uid, component);
  117. Popup.PopupEntity(Loc.GetString("vending-machine-restock-done", ("this", args.Args.Used), ("user", args.Args.User), ("target", uid)), args.Args.User, PopupType.Medium);
  118. Audio.PlayPvs(restockComponent.SoundRestockDone, uid, AudioParams.Default.WithVolume(-2f).WithVariation(0.2f));
  119. Del(args.Args.Used.Value);
  120. args.Handled = true;
  121. }
  122. /// <summary>
  123. /// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
  124. /// </summary>
  125. public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null)
  126. {
  127. if (!Resolve(uid, ref component))
  128. return;
  129. component.CanShoot = canShoot;
  130. }
  131. /// <summary>
  132. /// Sets the <see cref="VendingMachineComponent.Contraband"/> property of the vending machine.
  133. /// </summary>
  134. public void SetContraband(EntityUid uid, bool contraband, VendingMachineComponent? component = null)
  135. {
  136. if (!Resolve(uid, ref component))
  137. return;
  138. component.Contraband = contraband;
  139. Dirty(uid, component);
  140. }
  141. /// <summary>
  142. /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
  143. /// </summary>
  144. /// <param name="uid"></param>
  145. /// <param name="throwItem">Whether to throw the item in a random direction after dispensing it.</param>
  146. /// <param name="forceEject">Whether to skip the regular ejection checks and immediately dispense the item without animation.</param>
  147. /// <param name="vendComponent"></param>
  148. public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null)
  149. {
  150. if (!Resolve(uid, ref vendComponent))
  151. return;
  152. var availableItems = GetAvailableInventory(uid, vendComponent);
  153. if (availableItems.Count <= 0)
  154. return;
  155. var item = _random.Pick(availableItems);
  156. if (forceEject)
  157. {
  158. vendComponent.NextItemToEject = item.ID;
  159. vendComponent.ThrowNextItem = throwItem;
  160. var entry = GetEntry(uid, item.ID, item.Type, vendComponent);
  161. if (entry != null)
  162. entry.Amount--;
  163. EjectItem(uid, vendComponent, forceEject);
  164. }
  165. else
  166. {
  167. TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
  168. }
  169. }
  170. protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
  171. {
  172. if (!Resolve(uid, ref vendComponent))
  173. return;
  174. // No need to update the visual state because we never changed it during a forced eject
  175. if (!forceEject)
  176. TryUpdateVisualState((uid, vendComponent));
  177. if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
  178. {
  179. vendComponent.ThrowNextItem = false;
  180. return;
  181. }
  182. // Default spawn coordinates
  183. var spawnCoordinates = Transform(uid).Coordinates;
  184. //Make sure the wallvends spawn outside of the wall.
  185. if (TryComp<WallMountComponent>(uid, out var wallMountComponent))
  186. {
  187. var offset = wallMountComponent.Direction.ToWorldVec() * WallVendEjectDistanceFromWall;
  188. spawnCoordinates = spawnCoordinates.Offset(offset);
  189. }
  190. var ent = Spawn(vendComponent.NextItemToEject, spawnCoordinates);
  191. if (vendComponent.ThrowNextItem)
  192. {
  193. var range = vendComponent.NonLimitedEjectRange;
  194. var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range));
  195. _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce);
  196. }
  197. vendComponent.NextItemToEject = null;
  198. vendComponent.ThrowNextItem = false;
  199. }
  200. public override void Update(float frameTime)
  201. {
  202. base.Update(frameTime);
  203. var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
  204. while (disabled.MoveNext(out var uid, out _, out var comp))
  205. {
  206. if (comp.NextEmpEject < _timing.CurTime)
  207. {
  208. EjectRandom(uid, true, false, comp);
  209. comp.NextEmpEject += (5 * comp.EjectDelay);
  210. }
  211. }
  212. }
  213. public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null)
  214. {
  215. if (!Resolve(uid, ref vendComponent))
  216. return;
  217. RestockInventoryFromPrototype(uid, vendComponent);
  218. Dirty(uid, vendComponent);
  219. TryUpdateVisualState((uid, vendComponent));
  220. }
  221. private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
  222. {
  223. List<double> priceSets = new();
  224. // Find the most expensive inventory and use that as the highest price.
  225. foreach (var vendingInventory in component.CanRestock)
  226. {
  227. double total = 0;
  228. if (PrototypeManager.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype))
  229. {
  230. foreach (var (item, amount) in inventoryPrototype.StartingInventory)
  231. {
  232. if (PrototypeManager.TryIndex(item, out EntityPrototype? entity))
  233. total += _pricing.GetEstimatedPrice(entity) * amount;
  234. }
  235. }
  236. priceSets.Add(total);
  237. }
  238. args.Price += priceSets.Max();
  239. }
  240. private void OnEmpPulse(EntityUid uid, VendingMachineComponent component, ref EmpPulseEvent args)
  241. {
  242. if (!component.Broken && this.IsPowered(uid, EntityManager))
  243. {
  244. args.Affected = true;
  245. args.Disabled = true;
  246. component.NextEmpEject = _timing.CurTime;
  247. }
  248. }
  249. }
  250. }