diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index e88e4f473d..bda2e7710b 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -65,6 +65,7 @@ namespace Content.Client.Input human.AddFunction(ContentKeyFunctions.OpenEmotesMenu); human.AddFunction(ContentKeyFunctions.OpenInteractionMenu); /// Corvax-Wega human.AddFunction(ContentKeyFunctions.Strangle); /// Corvax-Wega + human.AddFunction(ContentKeyFunctions.OfferItem); /// Corvax-Wega-Offer human.AddFunction(ContentKeyFunctions.ActivateItemInWorld); human.AddFunction(ContentKeyFunctions.ThrowItemInHand); human.AddFunction(ContentKeyFunctions.AltActivateItemInWorld); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index c7b852b8c9..cbecde7149 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -186,6 +186,7 @@ namespace Content.Client.Options.UI.Tabs AddButton(ContentKeyFunctions.MoveStoredItem); AddButton(ContentKeyFunctions.RotateStoredItem); AddButton(ContentKeyFunctions.SaveItemLocation); + AddButton(ContentKeyFunctions.OfferItem); /// Corvax-Wega-Offer AddHeader("ui-options-header-interaction-adv"); AddButton(ContentKeyFunctions.SmartEquipBackpack); diff --git a/Content.Client/_Wega/Offer/OfferItemIndicatorsOverlay.cs b/Content.Client/_Wega/Offer/OfferItemIndicatorsOverlay.cs new file mode 100644 index 0000000000..318912d4fb --- /dev/null +++ b/Content.Client/_Wega/Offer/OfferItemIndicatorsOverlay.cs @@ -0,0 +1,38 @@ +using Content.Client.Resources; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Shared.Enums; + +namespace Content.Client.Offer; + +public sealed class OfferItemIndicatorsOverlay : Overlay +{ + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private readonly Texture _indicatorTexture; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + public OfferItemIndicatorsOverlay() + { + IoCManager.InjectDependencies(this); + + var resourceCache = IoCManager.Resolve(); + _indicatorTexture = resourceCache.GetTexture("/Textures/_Wega/Interface/Misc/give_item.rsi/give_item.png"); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var player = _player.LocalEntity; + if (player == null) + return; + + var screen = args.ScreenHandle; + var mousePos = _inputManager.MouseScreenPosition.Position; + + screen.DrawTexture(_indicatorTexture, mousePos - _indicatorTexture.Size / 2, Color.White); + } +} diff --git a/Content.Client/_Wega/Offer/OfferItemSystem.cs b/Content.Client/_Wega/Offer/OfferItemSystem.cs new file mode 100644 index 0000000000..cf63bafd8f --- /dev/null +++ b/Content.Client/_Wega/Offer/OfferItemSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Offer; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Offer; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private OfferItemIndicatorsOverlay? _overlayInstance; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGiverStartup); + SubscribeLocalEvent(OnGiverShutdown); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnComponentStateUpdated); + } + + private void OnGiverStartup(EntityUid uid, OfferGiverComponent component, ComponentStartup args) + { + UpdateOverlay(uid, component); + } + + private void OnGiverShutdown(EntityUid uid, OfferGiverComponent component, ComponentShutdown args) + { + UpdateOverlay(uid, component); + } + + private void OnPlayerAttached(EntityUid uid, OfferGiverComponent component, LocalPlayerAttachedEvent args) + { + UpdateOverlay(uid, component); + } + + private void OnPlayerDetached(EntityUid uid, OfferGiverComponent component, LocalPlayerDetachedEvent args) + { + UpdateOverlay(uid, component); + } + + private void OnComponentStateUpdated(EntityUid uid, OfferGiverComponent component, ref AfterAutoHandleStateEvent args) + { + UpdateOverlay(uid, component); + } + + private void UpdateOverlay(EntityUid uid, OfferGiverComponent component) + { + if (uid != _player.LocalEntity) + return; + + if (component.IsOffering) + { + if (_overlayInstance == null) + { + _overlayInstance = new(); + _overlay.AddOverlay(_overlayInstance); + } + } + else + { + if (_overlayInstance != null) + { + _overlay.RemoveOverlay(_overlayInstance); + _overlayInstance = null; + } + } + } +} diff --git a/Content.Server/_Wega/Offer/OfferItemSystem.cs b/Content.Server/_Wega/Offer/OfferItemSystem.cs new file mode 100644 index 0000000000..f353cf8d4a --- /dev/null +++ b/Content.Server/_Wega/Offer/OfferItemSystem.cs @@ -0,0 +1,106 @@ +using Content.Shared.Alert; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Offer; +using Content.Shared.Popups; + +namespace Content.Server.Offer; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnRequestToggleOffer); + + SubscribeLocalEvent(OnGiverInteractUsing); + SubscribeLocalEvent(OnReceiverAlertAcceptOffer); + } + + private void OnRequestToggleOffer(RequestToggleOfferEvent msg) + { + TryToggleOfferMode(GetEntity(msg.Player)); + } + + private void OnGiverInteractUsing(Entity ent, ref InteractUsingEvent args) + { + if (!TryComp(args.User, out var offer)) + return; + + if (!offer.IsOffering || args.User == args.Target || !HasComp(args.Target)) + return; + + if (!_transform.InRange(args.User, args.Target, offer.MaxOfferDistance)) + return; + + offer.Target = args.Target; + offer.IsOffering = false; + Dirty(args.User, offer); + + var receiver = EnsureComp(args.Target); + receiver.Offerer = args.User; + receiver.Item = offer.Item; + Dirty(args.Target, receiver); + + _alerts.ShowAlert(args.Target, receiver.Alert); + + if (offer.Item is { } item) + { + _popup.PopupEntity(Loc.GetString("offer-item-try-give", + ("item", Name(item)), ("target", Identity.Name(args.Target, EntityManager, args.User))), args.User, args.User); + + _popup.PopupEntity(Loc.GetString("offer-item-try-give-target", + ("user", Identity.Name(args.User, EntityManager, args.Target)), ("item", Name(item))), args.Target, args.Target); + } + } + + private void OnReceiverAlertAcceptOffer(EntityUid uid, OfferReceiverComponent component, AcceptOfferAlertEvent args) + { + if (args.AlertId != component.Alert || component.Offerer is not { } offerer) + return; + + TryAcceptOffer(uid, offerer); + } + + public bool TryAcceptOffer(EntityUid acceptor, EntityUid offerer) + { + if (!TryComp(offerer, out var offerComp) || !HasComp(acceptor)) + return false; + + if (_hands.GetEmptyHandCount(acceptor) == 0) + { + _popup.PopupEntity(Loc.GetString("offer-item-full-hand"), acceptor, acceptor); + CancelOffer(offerer, offerComp); + return false; + } + + if (offerComp.Item is { } item) + { + if (_hands.TryPickupAnyHand(acceptor, item)) + { + _popup.PopupEntity(Loc.GetString("offer-item-give", + ("item", Name(item)), ("target", Identity.Name(acceptor, EntityManager, offerer))), offerer, offerer); + + _popup.PopupEntity(Loc.GetString("offer-item-give-target", + ("item", Name(item)), ("target", Identity.Name(offerer, EntityManager, acceptor))), acceptor, acceptor); + } + else + { + _popup.PopupEntity(Loc.GetString("offer-item-no-give", + ("item", Name(item)), ("target", Identity.Name(acceptor, EntityManager, offerer))), offerer, offerer); + } + } + + CancelOffer(offerer, offerComp); + + return true; + } +} diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index d0848a3b7d..9684be3d9d 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -29,6 +29,7 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu"; public static readonly BoundKeyFunction OpenInteractionMenu = "OpenInteractionMenu"; /// Corvax-Wega public static readonly BoundKeyFunction Strangle = "Strangle"; /// Corvax-Wega + public static readonly BoundKeyFunction OfferItem = "OfferItem"; /// Corvax-Wega-Offer public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenGuidebook = "OpenGuidebook"; public static readonly BoundKeyFunction OpenInventoryMenu = "OpenInventoryMenu"; diff --git a/Content.Shared/_Wega/Offer/Components/OfferGiverComponent.cs b/Content.Shared/_Wega/Offer/Components/OfferGiverComponent.cs new file mode 100644 index 0000000000..c6faf7ed67 --- /dev/null +++ b/Content.Shared/_Wega/Offer/Components/OfferGiverComponent.cs @@ -0,0 +1,20 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Offer; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedOfferItemSystem))] +public sealed partial class OfferGiverComponent : Component +{ + [DataField, AutoNetworkedField] + public bool IsOffering = false; + + [DataField, AutoNetworkedField] + public EntityUid? Item; + + [DataField, AutoNetworkedField] + public EntityUid? Target; + + [DataField, AutoNetworkedField] + public float MaxOfferDistance = 2f; +} diff --git a/Content.Shared/_Wega/Offer/Components/OfferReceiverComponent.cs b/Content.Shared/_Wega/Offer/Components/OfferReceiverComponent.cs new file mode 100644 index 0000000000..84d2fa41b3 --- /dev/null +++ b/Content.Shared/_Wega/Offer/Components/OfferReceiverComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Alert; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Offer; + +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedOfferItemSystem))] +public sealed partial class OfferReceiverComponent : Component +{ + [DataField] + public EntityUid? Offerer; + + [DataField] + public EntityUid? Item; + + public ProtoId Alert = "Offer"; +} diff --git a/Content.Shared/_Wega/Offer/SharedOfferItemSystem.cs b/Content.Shared/_Wega/Offer/SharedOfferItemSystem.cs new file mode 100644 index 0000000000..7b84f39a9a --- /dev/null +++ b/Content.Shared/_Wega/Offer/SharedOfferItemSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Alert; +using Content.Shared.Hands; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Input; +using Content.Shared.Interaction.Components; +using Content.Shared.Popups; +using Robust.Shared.Input.Binding; +using Robust.Shared.Player; +using Robust.Shared.Serialization; + +namespace Content.Shared.Offer; + +public abstract partial class SharedOfferItemSystem : EntitySystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGiverMoved); + SubscribeLocalEvent(OnGiverItemUnequipped); + SubscribeLocalEvent(OnGiverTerminating); + + SubscribeLocalEvent(OnReceiverMoved); + SubscribeLocalEvent(OnReceiverTerminating); + SubscribeLocalEvent(OnReceiverShutdown); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.OfferItem, InputCmdHandler.FromDelegate(OnOfferItemCommand)) + .Register(); + } + + private void OnOfferItemCommand(ICommonSession? session) + { + if (session?.AttachedEntity is not { } player) + return; + + RaiseNetworkEvent(new RequestToggleOfferEvent(GetNetEntity(player))); + } + + public bool TryToggleOfferMode(EntityUid uid, OfferGiverComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return false; + + if (component.IsOffering || component.Target != null) + { + _popup.PopupEntity(Loc.GetString("offer-cancel-offer"), uid, uid); + CancelOffer(uid, component); + return true; + } + + if (!_hands.TryGetActiveItem(uid, out var item)) + { + _popup.PopupEntity(Loc.GetString("offer-item-empty-hand"), uid, uid); + return false; + } + + // You will not be able to transfer such items. + if (HasComp(item) || HasComp(item)) + return false; + + component.IsOffering = true; + component.Item = item; + Dirty(uid, component); + + return true; + } + + public void CancelOffer(EntityUid uid, OfferGiverComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; + + component.IsOffering = false; + component.Item = null; + + if (component.Target is { } target && HasComp(target)) + RemoveReceiverComponent(target, component); + + Dirty(uid, component); + } + + private void RemoveReceiverComponent(EntityUid uid, OfferGiverComponent component) + { + component.Target = null; + RemCompDeferred(uid); + } + + // TODO: Плохо что оно постоянно вызывается, возможно стоит переделеать в будущем. + private void OnGiverMoved(EntityUid uid, OfferGiverComponent component, MoveEvent args) + { + if (component.Target != null && !_transform.InRange(uid, component.Target.Value, component.MaxOfferDistance)) + CancelOffer(uid, component); + } + + private void OnGiverItemUnequipped(EntityUid uid, OfferGiverComponent component, DidUnequipHandEvent args) + { + if (args.Unequipped == component.Item) + CancelOffer(uid, component); + } + + private void OnGiverTerminating(EntityUid uid, OfferGiverComponent component, ref EntityTerminatingEvent args) + { + CancelOffer(uid, component); + } + + private void OnReceiverMoved(EntityUid uid, OfferReceiverComponent component, MoveEvent args) + { + if (component.Offerer != null && TryComp(component.Offerer, out var giver) + && !_transform.InRange(uid, component.Offerer.Value, giver.MaxOfferDistance)) + CancelOffer(component.Offerer.Value, giver); + } + + private void OnReceiverTerminating(EntityUid uid, OfferReceiverComponent component, ref EntityTerminatingEvent args) + { + if (component.Offerer is { } offerer && TryComp(offerer, out var giver)) + CancelOffer(offerer, giver); + } + + private void OnReceiverShutdown(EntityUid uid, OfferReceiverComponent component, ref ComponentShutdown args) + { + if (component.Offerer is { } offerer && TryComp(offerer, out var giver)) + CancelOffer(offerer, giver); + + _alerts.ClearAlert(uid, component.Alert); + } +} + +[Serializable, NetSerializable] +public sealed class RequestToggleOfferEvent : EntityEventArgs +{ + public NetEntity Player { get; } + + public RequestToggleOfferEvent(NetEntity player) + { + Player = player; + } +} + +public sealed partial class AcceptOfferAlertEvent : BaseAlertEvent; diff --git a/Resources/Locale/ru-RU/_wega/alerts/alerts.ftl b/Resources/Locale/ru-RU/_wega/alerts/alerts.ftl index 3a1dbbb97d..8b2a021f56 100644 --- a/Resources/Locale/ru-RU/_wega/alerts/alerts.ftl +++ b/Resources/Locale/ru-RU/_wega/alerts/alerts.ftl @@ -2,3 +2,5 @@ alerts-vampire-blood-name = Кровь alerts-vampire-blood-desc = Накопленная жизненная сила вампира позволяющая ему использовать сверх естественные способности. alerts-strangle-name = Душат alerts-strangle-desc = Вас [color=red]ДУШАТ[/color]. Щёлкните по иконке, чтобы попытаться выбраться +alerts-offer-name = Предложение +alerts-offer-desc = Кто-то предлагает вам взять предмет. diff --git a/Resources/Locale/ru-RU/_wega/escape-menu/ui/options-menu.ftl b/Resources/Locale/ru-RU/_wega/escape-menu/ui/options-menu.ftl index 857c21a6ac..22eea9c2be 100644 --- a/Resources/Locale/ru-RU/_wega/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/ru-RU/_wega/escape-menu/ui/options-menu.ftl @@ -1,3 +1,3 @@ ui-options-function-open-interaction-menu = Открыть меню взаимодействия -ui-options-function-toggle-crawling = Ползать/Встать ui-options-function-strangle = Душить +ui-options-function-offer-item = Переключить режим передачи предмета \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_wega/offer/offer.ftl b/Resources/Locale/ru-RU/_wega/offer/offer.ftl new file mode 100644 index 0000000000..e233f984af --- /dev/null +++ b/Resources/Locale/ru-RU/_wega/offer/offer.ftl @@ -0,0 +1,8 @@ +offer-item-empty-hand = Чтобы предложить предмет, нужно держать его в руке! +offer-cancel-offer = Вы отменили передачу предмета. +offer-item-try-give = Вы предложили {$item} {$target}. +offer-item-try-give-target = {$user} предлагает вам {$item}. +offer-item-give = Вы передали {$item} {$target}. +offer-item-give-target = {$target} передал вам {$item}. +offer-item-no-give = Не удалось передать {$item} {$target}. +offer-item-full-hand = Ваши руки заняты. diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 74e6263bfb..77229876d5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -175,6 +175,7 @@ - type: SSDIndicator - type: StandingState - type: SpeechSynthesis # Corvax-Wega-Barks + - type: OfferGiver # Corvax-Wega-Offer # - type: Dna # Corvax-Wega - type: MindContainer showExamineInfo: true diff --git a/Resources/Prototypes/_Wega/Alerts/alerts.yml b/Resources/Prototypes/_Wega/Alerts/alerts.yml new file mode 100644 index 0000000000..9985ba205c --- /dev/null +++ b/Resources/Prototypes/_Wega/Alerts/alerts.yml @@ -0,0 +1,8 @@ +- type: alert + id: Offer + clickEvent: !type:AcceptOfferAlertEvent + icons: + - sprite: /Textures/_Wega/Interface/Alerts/offer.rsi + state: offer + name: alerts-offer-name + description: alerts-offer-desc diff --git a/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/meta.json b/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/meta.json new file mode 100644 index 0000000000..0b9de65327 --- /dev/null +++ b/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken https://github.com/ss220-space/Paradise/blob/master220/icons/misc/mouse_icons/give_item.dmi and resprite by zekins3366", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "offer" + } + ] +} diff --git a/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/offer.png b/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/offer.png new file mode 100644 index 0000000000..bf868705c0 Binary files /dev/null and b/Resources/Textures/_Wega/Interface/Alerts/offer.rsi/offer.png differ diff --git a/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/give_item.png b/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/give_item.png new file mode 100644 index 0000000000..26bc88fb22 Binary files /dev/null and b/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/give_item.png differ diff --git a/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/meta.json b/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/meta.json new file mode 100644 index 0000000000..b104b943f5 --- /dev/null +++ b/Resources/Textures/_Wega/Interface/Misc/give_item.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Sprite taken from https://github.com/ss220-space/Paradise/blob/master220/icons/misc/mouse_icons/give_item.dmi", + "size": { + "x": 64, + "y": 64 + }, + "states": [ + { + "name": "give_item" + } + ] +} diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index d2e5393458..ff2b521dc7 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -201,6 +201,9 @@ binds: - function: Strangle type: State key: J +- function: OfferItem + type: State + key: F # Corvax-Wega-end - function: TextCursorSelect # TextCursorSelect HAS to be above ExamineEntity