diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index ee2ccbca0a..d3032c8720 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -8,6 +8,7 @@ using Content.Client.Interfaces.Chat; using Content.Client.Interfaces.Parallax; using Content.Client.Parallax; using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Markers; using Content.Shared.GameObjects.Components.Research; @@ -110,6 +111,7 @@ namespace Content.Client factory.Register(); factory.Register(); + factory.Register(); prototypes.RegisterIgnore("material"); diff --git a/Content.Client/GameObjects/Components/Wires/WiresBoundUserInterface.cs b/Content.Client/GameObjects/Components/Wires/WiresBoundUserInterface.cs new file mode 100644 index 0000000000..f6f9db605a --- /dev/null +++ b/Content.Client/GameObjects/Components/Wires/WiresBoundUserInterface.cs @@ -0,0 +1,37 @@ +using System; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using static Content.Shared.GameObjects.Components.SharedWiresComponent; + +namespace Content.Client.GameObjects.Components.Wires +{ + public class WiresBoundUserInterface : BoundUserInterface + { + public WiresBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private WiresMenu _menu; + + protected override void Open() + { + base.Open(); + _menu = new WiresMenu() {Owner = this}; + + _menu.OnClose += Close; + _menu.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + var castState = (WiresBoundUserInterfaceState) state; + _menu.Populate(castState.WiresList); + } + + public void PerformAction(Guid guid, WiresAction action) + { + SendMessage(new WiresActionMessage(guid, action)); + } + } +} diff --git a/Content.Client/GameObjects/Components/Wires/WiresMenu.cs b/Content.Client/GameObjects/Components/Wires/WiresMenu.cs new file mode 100644 index 0000000000..b05bfa95b8 --- /dev/null +++ b/Content.Client/GameObjects/Components/Wires/WiresMenu.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using static Content.Shared.GameObjects.Components.SharedWiresComponent; + +namespace Content.Client.GameObjects.Components.Wires +{ + public class WiresMenu : SS14Window + { +#pragma warning disable 649 + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + protected override Vector2? CustomSize => (300, 450); + public WiresBoundUserInterface Owner { get; set; } + + private readonly VBoxContainer _rows; + + public WiresMenu() + { + IoCManager.InjectDependencies(this); // TODO: Remove this and use DynamicTypeFactory? + Title = _localizationManager.GetString("Wires"); + _rows = new VBoxContainer(); + Contents.AddChild(_rows); + } + + public void Populate(List wiresList) + { + _rows.RemoveAllChildren(); + foreach (var entry in wiresList) + { + var container = new HBoxContainer(); + var newLabel = new Label() + { + Text = $"{_localizationManager.GetString(entry.Color.Name())}: ", + FontColorOverride = entry.Color, + }; + container.AddChild(newLabel); + + var newButton = new Button() + { + Text = _localizationManager.GetString("Pulse"), + }; + newButton.OnPressed += _ => Owner.PerformAction(entry.Guid, WiresAction.Pulse); + container.AddChild(newButton); + + newButton = new Button() + { + Text = entry.IsCut ? _localizationManager.GetString("Mend") : _localizationManager.GetString("Cut"), + }; + newButton.OnPressed += _ => Owner.PerformAction(entry.Guid, entry.IsCut ? WiresAction.Mend : WiresAction.Cut); + container.AddChild(newButton); + _rows.AddChild(container); + } + } + + } +} diff --git a/Content.Client/GameObjects/Components/Wires/WiresVisualizer2D.cs b/Content.Client/GameObjects/Components/Wires/WiresVisualizer2D.cs new file mode 100644 index 0000000000..5038f7fdf6 --- /dev/null +++ b/Content.Client/GameObjects/Components/Wires/WiresVisualizer2D.cs @@ -0,0 +1,25 @@ +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using static Content.Shared.GameObjects.Components.SharedWiresComponent; + +namespace Content.Client.GameObjects.Components.Wires +{ + public class WiresVisualizer2D : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + var sprite = component.Owner.GetComponent(); + if (component.TryGetData(WiresVisuals.MaintenancePanelState, out var state)) + { + sprite.LayerSetVisible(WiresVisualLayers.MaintenancePanel, state); + } + } + + public enum WiresVisualLayers + { + MaintenancePanel, + } + } +} diff --git a/Content.Server/GameObjects/Components/VendingMachines/VendingMachineComponent.cs b/Content.Server/GameObjects/Components/VendingMachines/VendingMachineComponent.cs index 8ca5248d84..288e218281 100644 --- a/Content.Server/GameObjects/Components/VendingMachines/VendingMachineComponent.cs +++ b/Content.Server/GameObjects/Components/VendingMachines/VendingMachineComponent.cs @@ -5,23 +5,28 @@ using Content.Shared.VendingMachines; using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.Interfaces.GameObjects; using Robust.Shared.GameObjects; -using Robust.Shared.GameObjects.Components.UserInterface; using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timers; using Robust.Shared.Utility; using System.Collections.Generic; +using System.Linq; using Content.Server.GameObjects.Components.Power; using Robust.Server.GameObjects; -using Robust.Shared.Log; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.Random; +using static Content.Shared.GameObjects.Components.SharedWiresComponent; namespace Content.Server.GameObjects.Components.VendingMachines { [RegisterComponent] [ComponentReference(typeof(IActivate))] - public class VendingMachineComponent : SharedVendingMachineComponent, IActivate, IExamine, IBreakAct + public class VendingMachineComponent : SharedVendingMachineComponent, IActivate, IExamine, IBreakAct, IWires { +#pragma warning disable 649 + [Dependency] private readonly IRobustRandom _random; +#pragma warning restore 649 private AppearanceComponent _appearance; private BoundUserInterface _userInterface; private PowerDeviceComponent _powerDevice; @@ -42,7 +47,14 @@ namespace Content.Server.GameObjects.Components.VendingMachines return; } - _userInterface.Open(actor.playerSession); + var wires = Owner.GetComponent(); + if (wires.IsOpen) + { + wires.OpenInterface(actor.playerSession); + } else + { + _userInterface.Open(actor.playerSession); + } } public override void ExposeData(ObjectSerializer serializer) @@ -190,6 +202,60 @@ namespace Content.Server.GameObjects.Components.VendingMachines _broken = true; TrySetVisualState(VendingMachineVisualState.Broken); } + + public enum Wires + { + /// + /// Shoots a random item when pulsed. + /// + Shoot + } + + void IWires.RegisterWires(WiresComponent.WiresBuilder builder) + { + builder.CreateWire(Wires.Shoot); + } + + void IWires.WiresUpdate(WiresUpdateEventArgs args) + { + var identifier = (Wires) args.Identifier; + if (identifier == Wires.Shoot && args.Action == WiresAction.Pulse) + { + EjectRandom(); + } + } + + /// + /// Ejects a random item if present. + /// + private void EjectRandom() + { + var availableItems = Inventory.Where(x => x.Amount > 0).ToList(); + if (availableItems.Count <= 0) + { + return; + } + TryEject(_random.Pick(availableItems).ID); + } + } + + public class WiresUpdateEventArgs : EventArgs + { + public readonly object Identifier; + public readonly WiresAction Action; + + public WiresUpdateEventArgs(object identifier, WiresAction action) + { + Identifier = identifier; + Action = action; + } + } + + public interface IWires + { + void RegisterWires(WiresComponent.WiresBuilder builder); + void WiresUpdate(WiresUpdateEventArgs args); + } } diff --git a/Content.Server/GameObjects/Components/WiresComponent.cs b/Content.Server/GameObjects/Components/WiresComponent.cs new file mode 100644 index 0000000000..03cf1cc41d --- /dev/null +++ b/Content.Server/GameObjects/Components/WiresComponent.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Interactable.Tools; +using Content.Server.GameObjects.Components.VendingMachines; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Server.Interfaces.GameObjects; +using Content.Shared.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.Components +{ + [RegisterComponent] + public class WiresComponent : SharedWiresComponent, IAttackBy, IExamine + { +#pragma warning disable 649 + [Dependency] private readonly IRobustRandom _random; + [Dependency] private readonly IServerNotifyManager _notifyManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + private AudioSystem _audioSystem; + private AppearanceComponent _appearance; + private BoundUserInterface _userInterface; + private bool _isOpen; + + public bool IsOpen + { + get => _isOpen; + private set + { + _isOpen = value; + _appearance.SetData(WiresVisuals.MaintenancePanelState, value); + } + } + + /// + /// Contains all registered wires. + /// + public readonly List WiresList = new List(); + + /// + /// As seen on /vg/station. + /// and . + /// + private readonly List _availableColors = new List() + { + Color.Red, + Color.Blue, + Color.Green, + Color.Orange, + Color.Brown, + Color.Gold, + Color.Gray, + Color.Cyan, + Color.Navy, + Color.Purple, + Color.Pink, + Color.Fuchsia, + Color.Aqua, + }; + + public override void Initialize() + { + base.Initialize(); + _audioSystem = IoCManager.Resolve().GetEntitySystem(); + _appearance = Owner.GetComponent(); + _appearance.SetData(WiresVisuals.MaintenancePanelState, IsOpen); + _userInterface = Owner.GetComponent() + .GetBoundUserInterface(WiresUiKey.Key); + _userInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; + + foreach (var wiresProvider in Owner.GetAllComponents()) + { + var builder = new WiresBuilder(this, wiresProvider); + wiresProvider.RegisterWires(builder); + } + + UpdateUserInterface(); + } + + public class Wire + { + /// + /// Used in client-server communication to identify a wire without telling the client what the wire does. + /// + public readonly Guid Guid; + /// + /// Registered by components implementing IWires, used to identify which wire the client interacted with. + /// + public readonly object Identifier; + /// + /// The color of the wire. It needs to have a corresponding entry in . + /// + public readonly Color Color; + /// + /// The component that registered the wire. + /// + public readonly IWires Owner; + /// + /// Whether the wire is cut. + /// + public bool IsCut; + public Wire(Guid guid, object identifier, Color color, IWires owner, bool isCut) + { + Guid = guid; + Identifier = identifier; + Color = color; + Owner = owner; + IsCut = isCut; + } + } + + /// + /// Used by . + /// + public class WiresBuilder + { + [NotNull] private readonly WiresComponent _wires; + [NotNull] private readonly IWires _owner; + + public WiresBuilder(WiresComponent wires, IWires owner) + { + _wires = wires; + _owner = owner; + } + + public void CreateWire(object identifier, Color? color = null, bool isCut = false) + { + if (!color.HasValue) + { + color = _wires.AssignColor(); + } + else + { + _wires._availableColors.Remove(color.Value); + } + _wires.WiresList.Add(new Wire(Guid.NewGuid(), identifier, color.Value, _owner, isCut)); + } + } + + /// + /// Picks a color from and removes it from the list. + /// + /// The picked color. + private Color AssignColor() + { + if(_availableColors.Count == 0) + { + return Color.Black; + } + return _random.PickAndTake(_availableColors); + } + + /// + /// Call this from other components to open the wires UI. + /// + public void OpenInterface(IPlayerSession session) + { + _userInterface.Open(session); + } + + private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) + { + var message = serverMsg.Message; + switch (message) + { + case WiresActionMessage msg: + var wire = WiresList.Find(x => x.Guid == msg.Guid); + var player = serverMsg.Session.AttachedEntity; + if (!player.TryGetComponent(out IHandsComponent handsComponent)) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, player, _localizationManager.GetString("You have no hands.")); + return; + } + var activeHandEntity = handsComponent.GetActiveHand?.Owner; + switch (msg.Action) + { + case WiresAction.Cut: + if (activeHandEntity?.HasComponent() != true) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, player, _localizationManager.GetString("You need to hold a wirecutter in your hand!")); + return; + } + _audioSystem.Play("/Audio/items/wirecutter.ogg", Owner); + wire.IsCut = true; + UpdateUserInterface(); + break; + case WiresAction.Mend: + if (activeHandEntity?.HasComponent() != true) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, player, _localizationManager.GetString("You need to hold a wirecutter in your hand!")); + return; + } + _audioSystem.Play("/Audio/items/wirecutter.ogg", Owner); + wire.IsCut = false; + UpdateUserInterface(); + break; + case WiresAction.Pulse: + if (activeHandEntity?.HasComponent() != true) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, player, _localizationManager.GetString("You need to hold a multitool in your hand!")); + return; + } + if (wire.IsCut) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, player, _localizationManager.GetString("You can't pulse a wire that's been cut!")); + return; + } + _audioSystem.Play("/Audio/effects/multitool_pulse.ogg", Owner); + break; + } + wire.Owner.WiresUpdate(new WiresUpdateEventArgs(wire.Identifier, msg.Action)); + break; + } + } + + private void UpdateUserInterface() + { + var clientList = new List(); + foreach (var entry in WiresList) + { + clientList.Add(new ClientWire(entry.Guid, entry.Color, entry.IsCut)); + } + _userInterface.SetState(new WiresBoundUserInterfaceState(clientList)); + } + + bool IAttackBy.AttackBy(AttackByEventArgs eventArgs) + { + if (!eventArgs.AttackWith.HasComponent()) return false; + IsOpen = !IsOpen; + return true; + } + + void IExamine.Examine(FormattedMessage message) + { + message.AddText($"The maintenance panel is {(IsOpen ? "open" : "closed")}."); + } + } +} diff --git a/Content.Shared/GameObjects/Components/SharedWiresComponent.cs b/Content.Shared/GameObjects/Components/SharedWiresComponent.cs new file mode 100644 index 0000000000..6011e73b20 --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedWiresComponent.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components +{ + public class SharedWiresComponent : Component + { + public override string Name => "Wires"; + + [Serializable, NetSerializable] + public enum WiresVisuals + { + MaintenancePanelState + } + + [Serializable, NetSerializable] + public enum WiresUiKey + { + Key, + } + + [Serializable, NetSerializable] + public enum WiresAction + { + Mend, + Cut, + Pulse, + } + + [Serializable, NetSerializable] + public class WiresBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly List WiresList; + + public WiresBoundUserInterfaceState(List wiresList) + { + WiresList = wiresList; + } + } + + [Serializable, NetSerializable] + public class ClientWire + { + public Guid Guid; + public Color Color; + public bool IsCut; + + public ClientWire(Guid guid, Color color, bool isCut) + { + Guid = guid; + Color = color; + IsCut = isCut; + } + } + + [Serializable, NetSerializable] + public class WiresActionMessage : BoundUserInterfaceMessage + { + public readonly Guid Guid; + public readonly WiresAction Action; + public WiresActionMessage(Guid guid, WiresAction action) + { + Guid = guid; + Action = action; + } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index ae8692f522..dcb8609547 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -24,5 +24,6 @@ public const uint MATERIAL_STORAGE = 1018; public const uint HAND_TELEPORTER = 1019; public const uint VENDING_MACHINE = 1020; + public const uint WIRES = 1021; } } diff --git a/Resources/Audio/effects/multitool_pulse.ogg b/Resources/Audio/effects/multitool_pulse.ogg new file mode 100644 index 0000000000..629f858ee9 Binary files /dev/null and b/Resources/Audio/effects/multitool_pulse.ogg differ diff --git a/Resources/Prototypes/Entities/buildings/vending_machines.yml b/Resources/Prototypes/Entities/buildings/vending_machines.yml index 99942a7597..54f890e871 100644 --- a/Resources/Prototypes/Entities/buildings/vending_machines.yml +++ b/Resources/Prototypes/Entities/buildings/vending_machines.yml @@ -8,6 +8,8 @@ layers: - state: normal map: ["enum.VendingMachineVisualLayers.Base"] + - texture: Buildings/maintenance_panel.png + map: ["enum.WiresVisualLayers.MaintenancePanel"] - type: Icon sprite: Buildings/VendingMachines/empty.rsi state: normal @@ -20,12 +22,16 @@ - type: Appearance visuals: - type: VendingMachineVisualizer2D + - type: WiresVisualizer2D - type: UserInterface interfaces: - key: enum.VendingMachineUiKey.Key type: VendingMachineBoundUserInterface + - key: enum.WiresUiKey.Key + type: WiresBoundUserInterface - type: PowerDevice priority: Low + - type: Wires - type: entity parent: VendingMachine diff --git a/Resources/Textures/Buildings/maintenance_panel.png b/Resources/Textures/Buildings/maintenance_panel.png new file mode 100644 index 0000000000..27dad11404 Binary files /dev/null and b/Resources/Textures/Buildings/maintenance_panel.png differ