diff --git a/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs b/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs new file mode 100644 index 00000000000..3b7f5744421 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/ShopVendorSystem.cs @@ -0,0 +1,123 @@ +using Content.Shared.DeltaV.VendingMachines; +using Content.Shared.VendingMachines; +using Robust.Client.Animations; +using Robust.Client.GameObjects; + +namespace Content.Client.DeltaV.VendingMachines; + +public sealed class ShopVendorSystem : SharedShopVendorSystem +{ + [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnAnimationCompleted); + } + + // copied from vending machines because its not reusable in other systems :) + private void OnAnimationCompleted(Entity ent, ref AnimationCompletedEvent args) + { + UpdateAppearance((ent, ent.Comp)); + } + + private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent args) + { + UpdateAppearance((ent, ent.Comp, args.Sprite)); + } + + private void UpdateAppearance(Entity ent) + { + if (!Resolve(ent, ref ent.Comp2)) + return; + + if (!_appearance.TryGetData(ent, VendingMachineVisuals.VisualState, out var state)) + state = VendingMachineVisualState.Normal; + + var sprite = ent.Comp2; + SetLayerState(VendingMachineVisualLayers.Base, ent.Comp1.OffState, sprite); + SetLayerState(VendingMachineVisualLayers.Screen, ent.Comp1.ScreenState, sprite); + switch (state) + { + case VendingMachineVisualState.Normal: + SetLayerState(VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.NormalState, sprite); + break; + + case VendingMachineVisualState.Deny: + if (ent.Comp1.LoopDenyAnimation) + SetLayerState(VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.DenyState, sprite); + else + PlayAnimation(ent, VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.DenyState, ent.Comp1.DenyDelay, sprite); + break; + + case VendingMachineVisualState.Eject: + PlayAnimation(ent, VendingMachineVisualLayers.BaseUnshaded, ent.Comp1.EjectState, ent.Comp1.EjectDelay, sprite); + break; + + case VendingMachineVisualState.Broken: + HideLayers(sprite); + SetLayerState(VendingMachineVisualLayers.Base, ent.Comp1.BrokenState, sprite); + break; + + case VendingMachineVisualState.Off: + HideLayers(sprite); + break; + } + } + + private static void SetLayerState(VendingMachineVisualLayers layer, string? state, SpriteComponent sprite) + { + if (state == null) + return; + + sprite.LayerSetVisible(layer, true); + sprite.LayerSetAutoAnimated(layer, true); + sprite.LayerSetState(layer, state); + } + + private void PlayAnimation(EntityUid uid, VendingMachineVisualLayers layer, string? state, TimeSpan time, SpriteComponent sprite) + { + if (state == null || _animationPlayer.HasRunningAnimation(uid, state)) + return; + + var animation = GetAnimation(layer, state, time); + sprite.LayerSetVisible(layer, true); + _animationPlayer.Play(uid, animation, state); + } + + private static Animation GetAnimation(VendingMachineVisualLayers layer, string state, TimeSpan time) + { + return new Animation + { + Length = time, + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = layer, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(state, 0f) + } + } + } + }; + } + + private static void HideLayers(SpriteComponent sprite) + { + HideLayer(VendingMachineVisualLayers.BaseUnshaded, sprite); + HideLayer(VendingMachineVisualLayers.Screen, sprite); + } + + private static void HideLayer(VendingMachineVisualLayers layer, SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(layer, out var actualLayer)) + return; + + sprite.LayerSetVisible(actualLayer, false); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs new file mode 100644 index 00000000000..6122aa9ee1e --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorBoundUserInterface.cs @@ -0,0 +1,25 @@ +using Content.Shared.DeltaV.VendingMachines; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +public sealed class ShopVendorBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private ShopVendorWindow? _window; + + public ShopVendorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.SetEntity(Owner); + _window.OpenCenteredLeft(); + _window.Title = EntMan.GetComponent(Owner).EntityName; + _window.OnItemSelected += index => SendMessage(new ShopVendorPurchaseMessage(index)); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml new file mode 100644 index 00000000000..4708db20aa9 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml @@ -0,0 +1,13 @@ + + + diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs new file mode 100644 index 00000000000..4a3c9c4efe5 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorItem.xaml.cs @@ -0,0 +1,21 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +[GenerateTypedNameReferences] +public sealed partial class ShopVendorItem : BoxContainer +{ + public ShopVendorItem(EntProtoId entProto, string text, uint cost) + { + RobustXamlLoader.Load(this); + + ItemPrototype.SetPrototype(entProto); + + NameLabel.Text = text; + + CostLabel.Text = cost.ToString(); + } +} diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml new file mode 100644 index 00000000000..cfc7f4a4a2c --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs new file mode 100644 index 00000000000..2b9c4df87a0 --- /dev/null +++ b/Content.Client/DeltaV/VendingMachines/UI/ShopVendorWindow.xaml.cs @@ -0,0 +1,147 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.DeltaV.VendingMachines; +using Content.Shared.Stacks; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using System.Numerics; + +namespace Content.Client.DeltaV.VendingMachines.UI; + +[GenerateTypedNameReferences] +public sealed partial class ShopVendorWindow : FancyWindow +{ + [Dependency] private readonly IComponentFactory _factory = default!; + [Dependency] private readonly IEntityManager _entMan = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + private readonly ShopVendorSystem _vendor; + + /// + /// Event fired with the listing index to purchase. + /// + public event Action? OnItemSelected; + + private EntityUid _owner; + private readonly StyleBoxFlat _style = new() { BackgroundColor = new Color(70, 73, 102) }; + private readonly StyleBoxFlat _styleBroke = new() { BackgroundColor = Color.FromHex("#303133") }; + private readonly List _buttons = new(); + private uint _balance = 1; + + public ShopVendorWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _vendor = _entMan.System(); + + VendingContents.SearchBar = SearchBar; + VendingContents.DataFilterCondition += DataFilterCondition; + VendingContents.GenerateItem += GenerateButton; + VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(((ShopVendorListingData) data).Index); + } + + public void SetEntity(EntityUid owner) + { + _owner = owner; + + if (!_entMan.TryGetComponent(owner, out var comp)) + return; + + var pack = _proto.Index(comp.Pack); + Populate(pack.Listings); + + UpdateBalance(); + } + + private void UpdateBalance(uint balance) + { + if (_balance == balance) + return; + + _balance = balance; + + BalanceLabel.Text = Loc.GetString("shop-vendor-balance", ("points", balance)); + + // disable items that are too expensive to buy + foreach (var button in _buttons) + { + if (button.Data is ShopVendorListingData data) + button.Disabled = data.Cost > balance; + + button.StyleBoxOverride = button.Disabled ? _styleBroke : _style; + } + } + + private void UpdateBalance() + { + if (_player.LocalEntity is {} user) + UpdateBalance(_vendor.GetBalance(_owner, user)); + } + + private bool DataFilterCondition(string filter, ListData data) + { + if (data is not ShopVendorListingData { Text: var text }) + return false; + + if (string.IsNullOrEmpty(filter)) + return true; + + return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase); + } + + private void GenerateButton(ListData data, ListContainerButton button) + { + if (data is not ShopVendorListingData cast) + return; + + _buttons.Add(button); + button.AddChild(new ShopVendorItem(cast.ItemId, cast.Text, cast.Cost)); + + button.ToolTip = cast.Text; + button.Disabled = cast.Cost > _balance; + button.StyleBoxOverride = button.Disabled ? _styleBroke : _style; + } + + public void Populate(List listings) + { + var longestEntry = string.Empty; + var listData = new List(); + for (var i = 0; i < listings.Count; i++) + { + var listing = listings[i]; + var proto = _proto.Index(listing.Id); + var text = proto.Name; + if (proto.TryGetComponent(out var stack, _factory) && stack.Count > 1) + { + text += " "; + text += Loc.GetString("shop-vendor-stack-suffix", ("count", stack.Count)); + } + listData.Add(new ShopVendorListingData(i, listing.Id, text, listing.Cost)); + } + + _buttons.Clear(); + VendingContents.PopulateList(listData); + SetSizeAfterUpdate(longestEntry.Length, listings.Count); + } + + private void SetSizeAfterUpdate(int longestEntryLength, int contentCount) + { + SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400), + Math.Clamp(contentCount * 50, 150, 350)); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + UpdateBalance(); + } +} + +public record ShopVendorListingData(int Index, EntProtoId ItemId, string Text, uint Cost) : ListData; diff --git a/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs b/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs new file mode 100644 index 00000000000..d3e99bfcf83 --- /dev/null +++ b/Content.Server/DeltaV/VendingMachines/ShopVendorSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Advertise; +using Content.Server.Advertise.Components; +using Content.Shared.DeltaV.VendingMachines; + +namespace Content.Server.DeltaV.VendingMachines; + +public sealed class ShopVendorSystem : SharedShopVendorSystem +{ + [Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + var now = Timing.CurTime; + while (query.MoveNext(out var uid, out var comp, out var xform)) + { + var ent = (uid, comp); + var dirty = false; + if (comp.Ejecting is {} ejecting && now > comp.NextEject) + { + Spawn(ejecting, xform.Coordinates); + comp.Ejecting = null; + dirty = true; + } + + if (comp.Denying && now > comp.NextDeny) + { + comp.Denying = false; + dirty = true; + } + + if (dirty) + { + Dirty(uid, comp); + UpdateVisuals(ent); + } + } + } + + protected override void AfterPurchase(Entity ent) + { + if (TryComp(ent, out var speak)) + _speakOnUIClosed.TrySetFlag((ent.Owner, speak)); + } +} diff --git a/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs b/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs new file mode 100644 index 00000000000..d505215a469 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/PointsVendorComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// +/// Makes a use mining points to buy items. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class PointsVendorComponent : Component; diff --git a/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs b/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs new file mode 100644 index 00000000000..464711c32ab --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/SharedShopVendorSystem.cs @@ -0,0 +1,181 @@ +using Content.Shared.Access.Systems; +using Content.Shared.DeltaV.Salvage.Systems; +using Content.Shared.Destructible; +using Content.Shared.Popups; +using Content.Shared.Power; +using Content.Shared.Power.EntitySystems; +using Content.Shared.UserInterface; +using Content.Shared.VendingMachines; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.DeltaV.VendingMachines; + +public abstract class SharedShopVendorSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _access = default!; + [Dependency] private readonly MiningPointsSystem _points = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPointLightSystem _light = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPowerReceiverSystem _power = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPointsBalance); + SubscribeLocalEvent(OnPointsPurchase); + + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnBreak); + SubscribeLocalEvent(OnOpenAttempt); + Subs.BuiEvents(VendingMachineUiKey.Key, subs => + { + subs.Event(OnPurchase); + }); + } + + #region Public API + + public uint GetBalance(EntityUid uid, EntityUid user) + { + var ev = new ShopVendorBalanceEvent(user); + RaiseLocalEvent(uid, ref ev); + return ev.Balance; + } + + #endregion + + #region Balance adapters + + private void OnPointsBalance(Entity ent, ref ShopVendorBalanceEvent args) + { + args.Balance = _points.TryFindIdCard(args.User)?.Comp?.Points ?? 0; + } + + private void OnPointsPurchase(Entity ent, ref ShopVendorPurchaseEvent args) + { + if (_points.TryFindIdCard(args.User) is {} idCard && _points.RemovePoints(idCard, args.Cost)) + args.Paid = true; + } + + #endregion + + private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) + { + UpdateVisuals(ent); + } + + private void OnBreak(Entity ent, ref BreakageEventArgs args) + { + ent.Comp.Broken = true; + UpdateVisuals(ent); + } + + private void OnOpenAttempt(Entity ent, ref ActivatableUIOpenAttemptEvent args) + { + if (ent.Comp.Broken) + args.Cancel(); + } + + private void OnPurchase(Entity ent, ref ShopVendorPurchaseMessage args) + { + if (ent.Comp.Ejecting != null || ent.Comp.Broken || !_power.IsPowered(ent.Owner)) + return; + + var pack = _proto.Index(ent.Comp.Pack); + if (args.Index < 0 || args.Index >= pack.Listings.Count) + return; + + var user = args.Actor; + if (!_access.IsAllowed(user, ent)) + { + Deny(ent, user); + return; + } + + var listing = pack.Listings[args.Index]; + var ev = new ShopVendorPurchaseEvent(user, listing.Cost); + RaiseLocalEvent(ent, ref ev); + if (!ev.Paid) + { + Deny(ent, user); + return; + } + + ent.Comp.Ejecting = listing.Id; + ent.Comp.NextEject = Timing.CurTime + ent.Comp.EjectDelay; + Dirty(ent); + + _audio.PlayPvs(ent.Comp.PurchaseSound, ent); + UpdateVisuals(ent); + + Log.Debug($"Player {ToPrettyString(user):user} purchased {listing.Id} from {ToPrettyString(ent):vendor}"); + + AfterPurchase(ent); + } + + protected virtual void AfterPurchase(Entity ent) + { + } + + private void Deny(Entity ent, EntityUid user) + { + _popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), ent, user); + if (ent.Comp.Denying) + return; + + ent.Comp.Denying = true; + ent.Comp.NextDeny = Timing.CurTime + ent.Comp.DenyDelay; + Dirty(ent); + + _audio.PlayPvs(ent.Comp.DenySound, ent); + UpdateVisuals(ent); + } + + protected void UpdateVisuals(Entity ent) + { + var state = VendingMachineVisualState.Normal; + var lit = true; + if (ent.Comp.Broken) + { + state = VendingMachineVisualState.Broken; + lit = false; + } + else if (ent.Comp.Ejecting != null) + { + state = VendingMachineVisualState.Eject; + } + else if (ent.Comp.Denying) + { + state = VendingMachineVisualState.Deny; + } + else if (!_power.IsPowered(ent.Owner)) + { + state = VendingMachineVisualState.Off; + lit = true; + } + + _light.SetEnabled(ent, lit); + _appearance.SetData(ent, VendingMachineVisuals.VisualState, state); + } +} + +/// +/// Raised on a shop vendor to get its current balance. +/// A currency component sets Balance to whatever it is. +/// +[ByRefEvent] +public record struct ShopVendorBalanceEvent(EntityUid User, uint Balance = 0); + +/// +/// Raised on a shop vendor when trying to purchase an item. +/// A currency component sets Paid to true if the user successfully paid for it. +/// +[ByRefEvent] +public record struct ShopVendorPurchaseEvent(EntityUid User, uint Cost, bool Paid = false); diff --git a/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs b/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs new file mode 100644 index 00000000000..3b04c0d0490 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopInventoryPrototype.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// +/// Similar to VendingMachineInventoryPrototype but for . +/// +[Prototype] +public sealed class ShopInventoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The item listings for sale. + /// + [DataField(required: true)] + public List Listings = new(); +} + +[DataRecord, Serializable] +public record struct ShopListing(EntProtoId Id, uint Cost); diff --git a/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs b/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs new file mode 100644 index 00000000000..1de2c5476ed --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopVendorComponent.cs @@ -0,0 +1,96 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.DeltaV.VendingMachines; + +/// +/// A vending machine that sells items for a currency controlled by events. +/// Does not need restocking. +/// Another component must handle and to work. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedShopVendorSystem))] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class ShopVendorComponent : Component +{ + /// + /// The inventory prototype to sell. + /// + [DataField(required: true)] + public ProtoId Pack; + + [DataField, AutoNetworkedField] + public bool Broken; + + [DataField, AutoNetworkedField] + public bool Denying; + + /// + /// Item being ejected, or null if it isn't. + /// + [DataField, AutoNetworkedField] + public EntProtoId? Ejecting; + + /// + /// How long to wait before flashing denied again. + /// + [DataField] + public TimeSpan DenyDelay = TimeSpan.FromSeconds(2); + + /// + /// How long to wait before another item can be bought + /// + [DataField] + public TimeSpan EjectDelay = TimeSpan.FromSeconds(1.2); + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextDeny; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextEject; + + [DataField] + public SoundSpecifier PurchaseSound = new SoundPathSpecifier("/Audio/Machines/machine_vend.ogg") + { + Params = new AudioParams + { + Volume = -4f, + Variation = 0.15f + } + }; + + [DataField] + public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg") + { + Params = new AudioParams + { + Volume = -2f + } + }; + + #region Visuals + + [DataField] + public bool LoopDenyAnimation = true; + + [DataField] + public string? OffState; + + [DataField] + public string? ScreenState; + + [DataField] + public string? NormalState; + + [DataField] + public string? DenyState; + + [DataField] + public string? EjectState; + + [DataField] + public string? BrokenState; + + #endregion +} diff --git a/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs b/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs new file mode 100644 index 00000000000..9f288d9d322 --- /dev/null +++ b/Content.Shared/DeltaV/VendingMachines/ShopVendorUI.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.DeltaV.VendingMachines; + +[Serializable, NetSerializable] +public sealed class ShopVendorPurchaseMessage(int index) : BoundUserInterfaceMessage +{ + public readonly int Index = index; +} diff --git a/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl b/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl new file mode 100644 index 00000000000..361d1dc71cb --- /dev/null +++ b/Resources/Locale/en-US/deltav/vending-machines/shop-vendor.ftl @@ -0,0 +1,4 @@ +shop-vendor-balance = Balance: {$points} +shop-vendor-stack-suffix = x{$count} +shop-vendor-flavor-left = All payments are secure +shop-vendor-flavor-right = v1.2 diff --git a/Resources/Migrations/deltaMigrations.yml b/Resources/Migrations/deltaMigrations.yml index 18a7472447d..d0c682215f9 100644 --- a/Resources/Migrations/deltaMigrations.yml +++ b/Resources/Migrations/deltaMigrations.yml @@ -128,3 +128,6 @@ SuitStorageSec: SuitStorageSecDeltaV LightBulbMaintenanceRed: DimLightBulb PoweredSmallLightMaintenanceRed: PoweredDimSmallLight AlwaysPoweredSmallLightMaintenanceRed: PoweredDimSmallLight + +# 2024-12-22 +VendingMachineRestockSalvageEquipment: null diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml index d43dc856f21..a3263dde32b 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_vending.yml @@ -148,15 +148,15 @@ category: cargoproduct-category-name-service group: market -- type: cargoProduct - id: CrateVendingMachineRestockSalvageEquipment - icon: - sprite: Objects/Specific/Service/vending_machine_restock.rsi - state: base - product: CrateVendingMachineRestockSalvageEquipmentFilled - cost: 1000 - category: cargoproduct-category-name-engineering - group: market +#- type: cargoProduct # DeltaV: Salvage vendor doesn't have stock anymore +# id: CrateVendingMachineRestockSalvageEquipment +# icon: +# sprite: Objects/Specific/Service/vending_machine_restock.rsi +# state: base +# product: CrateVendingMachineRestockSalvageEquipmentFilled +# cost: 1000 +# category: cargoproduct-category-name-engineering +# group: market - type: cargoProduct id: CrateVendingMachineRestockSecTech diff --git a/Resources/Prototypes/Catalog/Fills/Crates/vending.yml b/Resources/Prototypes/Catalog/Fills/Crates/vending.yml index 378f8cb7588..606d538541e 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/vending.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/vending.yml @@ -140,15 +140,15 @@ - id: VendingMachineRestockRobustSoftdrinks amount: 2 -- type: entity - id: CrateVendingMachineRestockSalvageEquipmentFilled - parent: CrateGenericSteel - name: Salvage restock crate - description: Contains a restock box for the salvage vendor. - components: - - type: StorageFill - contents: - - id: VendingMachineRestockSalvageEquipment +#- type: entity # DeltaV: Salvage vendor doesn't have stock anymore +# id: CrateVendingMachineRestockSalvageEquipmentFilled +# parent: CrateGenericSteel +# name: Salvage restock crate +# description: Contains a restock box for the salvage vendor. +# components: +# - type: StorageFill +# contents: +# - id: VendingMachineRestockSalvageEquipment - type: entity id: CrateVendingMachineRestockSecTechFilled diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml index fc681630e66..7761453327a 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/salvage.yml @@ -3,14 +3,10 @@ startingInventory: Crowbar: 2 Pickaxe: 4 - OreBag: 4 # DeltaV - more ore bags, was 2 + OreBag: 4 Flare: 4 FlashlightLantern: 2 - Floodlight: 2 # DeltaV - upstream removed them HandheldGPSBasic: 2 RadioHandheld: 2 - #WeaponGrapplingGun: 2 # DeltaV - removed from vendor due to physics funkiness + WeaponGrapplingGun: 4 WeaponProtoKineticAccelerator: 4 - SeismicCharge: 2 # DeltaV - upstream removed - FultonBeacon: 1 # DeltaV - upstream removed - Fulton: 2 # DeltaV - upstream removed diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml new file mode 100644 index 00000000000..eb0684b396f --- /dev/null +++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Items/Backpacks/duffelbag.yml @@ -0,0 +1,18 @@ +- type: entity + parent: ClothingBackpackDuffelSalvage + id: ClothingBackpackDuffelSalvageConscription + name: mining conscription kit + description: A duffel bag containing everything a crewmember needs to support a shaft miner in the field. + components: + - type: StorageFill + contents: + - id: ClothingEyesGlassesMeson + - id: MineralScanner + - id: OreBag + - id: ClothingUniformJumpsuitSalvageSpecialist + - id: EncryptionKeyCargo + - id: ClothingMaskGasExplorer + - id: SalvageIDCard + - id: WeaponProtoKineticAccelerator + - id: SurvivalKnife + - id: FlashlightSeclite diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Lockers/cargo.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Lockers/cargo.yml index 62f45b5aaf4..da266059d9c 100644 --- a/Resources/Prototypes/DeltaV/Catalog/Fills/Lockers/cargo.yml +++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Lockers/cargo.yml @@ -3,9 +3,12 @@ id: LockerFillSalvageSpecialistDeltaV table: !type:AllSelector children: - - id: SeismicCharge - - id: SeismicCharge - id: OreBag + - id: Pickaxe + - id: WeaponProtoKineticAccelerator + - id: FlashlightSeclite + - id: ClothingEyesGlassesMeson + # TODO: bluespace shelter capsule - id: ClothingShoesBootsWinterMiner - id: JetpackMiniFilled # replaces fire extinguisher - id: ClothingNeckSalvager diff --git a/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml b/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml new file mode 100644 index 00000000000..c1558ca8e50 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Catalog/VendingMachines/Inventories/salvage_points.yml @@ -0,0 +1,53 @@ +- type: shopInventory + id: SalvageVendorInventory + listings: + # TODO: marker beacons 1/10/30 for 10 each + - id: DrinkWhiskeyBottleFull + cost: 100 + - id: DrinkAbsintheBottleFull + cost: 100 + - id: CigarGold + cost: 150 + - id: Soap + cost: 200 + - id: SeismicCharge + cost: 250 + - id: WeaponGrapplingGun + cost: 300 + # TODO: laser pointer 300, toy facehugger 300 + # TODO: stabilizing serum for 400 + - id: FultonBeacon + cost: 400 + # TODO: bluespace shelter capsule for 400 + - id: ClothingEyesGlassesGarMeson + cost: 500 + - id: ClothingBeltSalvageWebbing + cost: 500 + - id: MedkitBruteFilled + cost: 600 + - id: MedkitBurnFilled + cost: 600 + # TODO: salvage 5g, 3 implants and a locator for 600 + # TODO: wormhole jaunter for 750 + - id: WeaponCrusher + cost: 750 + - id: WeaponProtoKineticAccelerator + cost: 750 + - id: AdvancedMineralScanner + cost: 800 + # TODO: resonator for 800 + - id: Fulton + cost: 1000 + # TODO: lazarus injector for 1k + - id: ClothingBackpackDuffelSalvageConscription + cost: 1500 + - id: SpaceCash1000 + cost: 2000 + # TODO: super resonator for 2500 + # TODO: jump boots for 2500 + - id: ClothingOuterHardsuitSalvage + cost: 3000 + # TODO: luxury shelter capsule for 3k + # TODO: luxury elite bar capsule for 10k + # TODO: pka mods + # TODO: mining drone stuff diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml index 01fb7dadafd..bf7fc3cf3d0 100644 --- a/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Eyes/glasses.yml @@ -94,3 +94,11 @@ sprite: DeltaV/Clothing/Eyes/Glasses/interdynechemgoogles.rsi - type: Clothing sprite: DeltaV/Clothing/Eyes/Glasses/interdynechemgoogles.rsi + +- type: entity + parent: ClothingEyesGlassesGar + id: ClothingEyesGlassesGarMeson + name: gar mesons + description: Do the impossible, see the invisible! + components: + - type: EyeProtection diff --git a/Resources/Prototypes/DeltaV/Wires/layouts.yml b/Resources/Prototypes/DeltaV/Wires/layouts.yml index fd1e77dc898..90d781119a8 100644 --- a/Resources/Prototypes/DeltaV/Wires/layouts.yml +++ b/Resources/Prototypes/DeltaV/Wires/layouts.yml @@ -7,3 +7,11 @@ - !type:DoorBoltLightWireAction - !type:LogWireAction - !type:AiInteractWireAction + +- type: wireLayout + id: ShopVendor + wires: + - !type:AiInteractWireAction + - !type:PowerWireAction + - !type:AccessWireAction + - !type:LogWireAction diff --git a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml index 0306a7dc147..9b0a4361f9f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml @@ -17,7 +17,7 @@ slots: - belt - type: Item - size: Ginormous + size: Huge # DeltaV: Was Ginormous, lets it fit in conscription bag - type: Storage maxItemSize: Normal grid: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml b/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml index c7851aff88d..1254e95fc07 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Service/vending_machine_restock.yml @@ -361,6 +361,7 @@ - state: refill_sec - type: entity + abstract: true # DeltaV: Salvage vendor doesn't have stock anymore parent: BaseVendingMachineRestock id: VendingMachineRestockSalvageEquipment name: Salvage Vendor restock box diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 7b88556d195..3ecb065518a 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -1356,12 +1356,12 @@ name: Salvage Vendor description: A dwarf's best friend! components: - - type: VendingMachine - pack: SalvageEquipmentInventory - offState: off - brokenState: broken - normalState: normal-unshaded - denyState: deny-unshaded + #- type: VendingMachine # DeltaV: Use mining points instead of limited stock + # pack: SalvageEquipmentInventory + # offState: off + # brokenState: broken + # normalState: normal-unshaded + # denyState: deny-unshaded - type: Sprite sprite: Structures/Machines/VendingMachines/mining.rsi layers: @@ -1376,6 +1376,21 @@ radius: 1.5 energy: 1.6 color: "#b89f25" + - type: ShopVendor # DeltaV + pack: SalvageVendorInventory + offState: off + brokenState: broken + normalState: normal-unshaded + denyState: deny-unshaded + - type: PointsVendor # DeltaV + - type: UserInterface # DeltaV: Replace vending machine BUI with shop vendor + interfaces: + enum.VendingMachineUiKey.Key: + type: ShopVendorBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface + - type: Wires # DeltaV: Use shop vendor wires layout + layoutId: ShopVendor - type: AccessReader access: [["Salvage"]] - type: GuideHelp