using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; using Content.Server.Power.Components; using Content.Server.Vocalization.Systems; using Content.Shared.Cargo; using Content.Shared.Damage; using Content.Shared.Destructible; using Content.Shared.Emp; using Content.Shared.Power; using Content.Shared.Throwing; using Content.Shared.UserInterface; using Content.Shared.VendingMachines; using Content.Shared.Wall; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.VendingMachines { public sealed class VendingMachineSystem : SharedVendingMachineSystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly ThrowingSystem _throwingSystem = default!; private const float WallVendEjectDistanceFromWall = 1f; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnBreak); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnVendingPrice); SubscribeLocalEvent(OnTryVocalize); SubscribeLocalEvent(OnActivatableUIOpenAttempt); SubscribeLocalEvent(OnSelfDispense); SubscribeLocalEvent(OnPriceCalculation); } private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args) { var price = 0.0; foreach (var entry in component.Inventory.Values) { if (!PrototypeManager.TryIndex(entry.ID, out var proto)) { Log.Error($"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending."); continue; } price += entry.Amount * _pricing.GetEstimatedPrice(proto); } args.Price += price; } protected override void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args) { base.OnMapInit(uid, component, args); if (HasComp(uid)) { TryUpdateVisualState((uid, component)); } } private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args) { if (component.Broken) args.Cancel(); } private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args) { TryUpdateVisualState((uid, component)); } private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs) { vendComponent.Broken = true; Dirty(uid, vendComponent); TryUpdateVisualState((uid, vendComponent)); } private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args) { if (!args.DamageIncreased && component.Broken) { component.Broken = false; Dirty(uid, component); TryUpdateVisualState((uid, component)); return; } if (component.Broken || component.DispenseOnHitCoolingDown || component.DispenseOnHitChance == null || args.DamageDelta == null) return; if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold && _random.Prob(component.DispenseOnHitChance.Value)) { if (component.DispenseOnHitCooldown != null) { component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value; } EjectRandom(uid, throwItem: true, forceEject: true, component); } } private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args) { if (args.Handled) return; args.Handled = true; EjectRandom(uid, throwItem: true, forceEject: false, component); } /// /// Sets the property of the vending machine. /// public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return; component.CanShoot = canShoot; } /// /// Sets the property of the vending machine. /// public void SetContraband(EntityUid uid, bool contraband, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return; component.Contraband = contraband; Dirty(uid, component); } /// /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty. /// /// /// Whether to throw the item in a random direction after dispensing it. /// Whether to skip the regular ejection checks and immediately dispense the item without animation. /// public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; var availableItems = GetAvailableInventory(uid, vendComponent); if (availableItems.Count <= 0) return; var item = _random.Pick(availableItems); if (forceEject) { vendComponent.NextItemToEject = item.ID; vendComponent.ThrowNextItem = throwItem; var entry = GetEntry(uid, item.ID, item.Type, vendComponent); if (entry != null) entry.Amount--; EjectItem(uid, vendComponent, forceEject); } else { TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent); } } protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { if (!Resolve(uid, ref vendComponent)) return; // No need to update the visual state because we never changed it during a forced eject if (!forceEject) TryUpdateVisualState((uid, vendComponent)); if (string.IsNullOrEmpty(vendComponent.NextItemToEject)) { vendComponent.ThrowNextItem = false; return; } // Default spawn coordinates var xform = Transform(uid); var spawnCoordinates = xform.Coordinates; //Make sure the wallvends spawn outside of the wall. if (TryComp(uid, out var wallMountComponent)) { var offset = (wallMountComponent.Direction + xform.LocalRotation - Math.PI / 2).ToVec() * WallVendEjectDistanceFromWall; spawnCoordinates = spawnCoordinates.Offset(offset); } var ent = Spawn(vendComponent.NextItemToEject, spawnCoordinates); if (vendComponent.ThrowNextItem) { var range = vendComponent.NonLimitedEjectRange; var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range)); _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce); } vendComponent.NextItemToEject = null; vendComponent.ThrowNextItem = false; } public override void Update(float frameTime) { base.Update(frameTime); var disabled = EntityQueryEnumerator(); while (disabled.MoveNext(out var uid, out _, out var comp)) { if (comp.NextEmpEject < Timing.CurTime) { EjectRandom(uid, true, false, comp); comp.NextEmpEject += (5 * comp.EjectDelay); } } } private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args) { List priceSets = new(); // Find the most expensive inventory and use that as the highest price. foreach (var vendingInventory in component.CanRestock) { double total = 0; if (PrototypeManager.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype)) { foreach (var (item, amount) in inventoryPrototype.StartingInventory) { if (PrototypeManager.TryIndex(item, out EntityPrototype? entity)) total += _pricing.GetEstimatedPrice(entity) * amount; } } priceSets.Add(total); } args.Price += priceSets.Max(); } private void OnTryVocalize(Entity ent, ref TryVocalizeEvent args) { args.Cancelled |= ent.Comp.Broken; } } }