diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index e2ccb39f534..d9f5f22f79c 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -133,3 +133,5 @@
#define STATUS_EFFECT_SYPHONMARK /datum/status_effect/syphon_mark //tracks kills for the KA death syphon module
#define STATUS_EFFECT_ADAPTIVELEARNING /datum/status_effect/adaptive_learning //tracks the total bonus damage needed to be done to target
+/// Status effect given when someone uses the Give Item command to offer an item to another player.
+#define STATUS_EFFECT_OFFERING_ITEM /datum/status_effect/offering_item
diff --git a/code/__HELPERS/qdel.dm b/code/__HELPERS/qdel.dm
index 020077a4e5c..99ccce4719b 100644
--- a/code/__HELPERS/qdel.dm
+++ b/code/__HELPERS/qdel.dm
@@ -8,3 +8,15 @@
/proc/______qdel_list_wrapper(list/L) //the underscores are to encourage people not to use this directly.
QDEL_LIST(L)
+
+/**
+ * # Signal qdel
+ *
+ * Proc intended to be used when someone wants the src datum to be qdeled when a certain signal is sent to them.
+ *
+ * Example usage:
+ * RegisterSignal(item, COMSIG_PARENT_QDELETING, /datum/proc/signal_qdel)
+ */
+/datum/proc/signal_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 0e70c02b05d..1038ed2c18f 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -3,19 +3,21 @@
//PUBLIC - call these wherever you want
-/mob/proc/throw_alert(category, type, severity, obj/new_master, override = FALSE, timeout_override, no_anim, icon_override)
-
-/*
- Proc to create or update an alert. Returns the alert if the alert is new or updated, 0 if it was thrown already
- category is a text string. Each mob may only have one alert per category; the previous one will be replaced
- path is a type path of the actual alert type to throw
- severity is an optional number that will be placed at the end of the icon_state for this alert
- For example, high pressure's icon_state is "highpressure" and can be serverity 1 or 2 to get "highpressure1" or "highpressure2"
- new_master is optional and sets the alert's icon state to "template" in the ui_style icons with the master as an overlay.
- Clicks are forwarded to master
- Override makes it so the alert is not replaced until cleared by a clear_alert with clear_override, and it's used for hallucinations.
+/**
+ * Proc to create or update an alert. Returns the alert if the alert is new or updated, 0 if it was thrown already.
+ * Each mob may only have one alert per category.
+ *
+ * Arguments:
+ * * category - a text string corresponding to what type of alert it is
+ * * type - a type path of the actual alert type to throw
+ * * severity - is an optional number that will be placed at the end of the icon_state for this alert
+ For example, high pressure's icon_state is "highpressure" and can be serverity 1 or 2 to get "highpressure1" or "highpressure2"
+ * * obj/new_master - optional argument. Sets the alert's icon state to "template" in the ui_style icons with the master as an overlay. Clicks are forwarded to master
+ * * no_anim - whether the alert should play a small sliding animation when created on the player's screen
+ * * icon_override - makes it so the alert is not replaced until cleared by a clear_alert with clear_override, and it's used for hallucinations.
+ * * list/alert_args - a list of arguments to pass to the alert when creating it
*/
-
+/mob/proc/throw_alert(category, type, severity, obj/new_master, override = FALSE, timeout_override, no_anim, icon_override, list/alert_args)
if(!category)
return
@@ -37,7 +39,11 @@
else //no need to update
return 0
else
- alert = new type()
+ if(alert_args)
+ alert_args.Insert(1, null) // So it's still created in nullspace.
+ alert = new type(arglist(alert_args))
+ else
+ alert = new type()
alert.override_alerts = override
if(override)
alert.timeout = null
diff --git a/code/datums/keybindings/carbon.dm b/code/datums/keybindings/carbon.dm
index b27a59fbc73..69aaa35b136 100644
--- a/code/datums/keybindings/carbon.dm
+++ b/code/datums/keybindings/carbon.dm
@@ -22,6 +22,15 @@
var/mob/living/carbon/M = C.mob
M.throw_mode_off()
+/datum/keybinding/carbon/give_item
+ name = "Give Item (Toggle)"
+ keys = null
+
+/datum/keybinding/carbon/give_item/down(client/C)
+ . = ..()
+ var/mob/living/carbon/user = C.mob
+ user.toggle_give()
+
/datum/keybinding/carbon/intent
/// The intent to switch to.
var/intent
diff --git a/code/modules/mob/living/carbon/give.dm b/code/modules/mob/living/carbon/give.dm
index 6795ec8643c..59d06a9fdd5 100644
--- a/code/modules/mob/living/carbon/give.dm
+++ b/code/modules/mob/living/carbon/give.dm
@@ -1,52 +1,189 @@
-/mob/living/carbon/verb/give(mob/living/carbon/target in oview(1))
+/**
+ * Toggles the [/datum/click_intercept/give] on or off for the src mob.
+ */
+/mob/living/carbon/verb/toggle_give()
+ set name = "Give Item"
set category = "IC"
- set name = "Give"
- if(!iscarbon(target)) //something is bypassing the give arguments, no clue what, adding a sanity check JIC
- to_chat(usr, "Wait a second... \the [target] HAS NO HANDS! AHH!")//cheesy messages ftw
+ if(has_status_effect(STATUS_EFFECT_OFFERING_ITEM))
+ to_chat(src, "You're already offering an item to someone!")
return
-
- if(target.incapacitated() || usr.incapacitated() || target.client == null)
+ if(istype(client.click_intercept, /datum/click_intercept/give))
+ QDEL_NULL(client.click_intercept)
return
-
var/obj/item/I = get_active_hand()
-
if(!I)
- to_chat(usr, " You don't have anything in your hand to give to [target.name]")
+ to_chat(src, "You don't have anything in your hand to give!")
return
- if((I.flags & NODROP) || (I.flags & ABSTRACT))
- to_chat(usr, "That's not exactly something you can give.")
+ if(I.flags & NODROP)
+ to_chat(src, "[I] is stuck to your hand, you can't give it away!")
return
- if(target.r_hand == null || target.l_hand == null)
- var/ans = alert(target,"[usr] wants to give you \a [I]?",,"Yes","No")
- if(!I || !target)
- return
- switch(ans)
- if("Yes")
- if(target.incapacitated() || usr.incapacitated())
- return
- if(!Adjacent(target))
- to_chat(usr, " You need to stay in reaching distance while giving an object.")
- to_chat(target, " [usr.name] moved too far away.")
- return
- if((I.flags & NODROP) || (I.flags & ABSTRACT))
- to_chat(usr, "[I] stays stuck to your hand when you try to give it!")
- to_chat(target, "[I] stays stuck to [usr.name]'s hand when you try to take it!")
- return
- if(I != get_active_hand())
- to_chat(usr, " You need to keep the item in your active hand.")
- to_chat(target, " [usr.name] seem to have given up on giving [I] to you.")
- return
- if(target.r_hand != null && target.l_hand != null)
- to_chat(target, " Your hands are full.")
- to_chat(usr, " Their hands are full.")
- return
- usr.unEquip(I)
- target.put_in_hands(I)
- I.add_fingerprint(target)
- target.visible_message(" [usr.name] handed [I] to [target.name].")
- I.on_give(usr, target)
- if("No")
- target.visible_message(" [usr.name] tried to hand [I] to [target.name] but [target.name] didn't want it.")
- else
- to_chat(usr, " [target.name]'s hands are full.")
+ if(I.flags & ABSTRACT)
+ to_chat(src, "That's not exactly something you can give.")
+ return
+
+ new /datum/click_intercept/give(client)
+
+/**
+ * # Offering Item status effect
+ *
+ * Status effect given to mobs after they've offered an item to another player using the Give Item action ([/datum/click_intercept/give]).
+ */
+/datum/status_effect/offering_item
+ id = "offering item"
+ duration = 10 SECONDS
+ alert_type = /obj/screen/alert/status_effect/offering_item
+
+/datum/status_effect/offering_item/on_creation(mob/living/new_owner, receiver_UID, item_UID)
+ . = ..()
+ var/obj/screen/alert/status_effect/offering_item/offer = linked_alert
+ offer.item_UID = item_UID
+ offer.receiver_UID = receiver_UID
+
+
+/obj/screen/alert/status_effect/offering_item
+ name = "Offering Item"
+ desc = "You're currently offering an item someone. Make sure to keep the item in your hand so they can accept it! Click to stop offering your item."
+ icon_state = "offering_item"
+ /// UID of the mob who's being offered the item.
+ var/receiver_UID
+ /// UID of the item being given.
+ var/item_UID
+
+/obj/screen/alert/status_effect/offering_item/Click(location, control, params)
+ var/mob/living/carbon/receiver = locateUID(receiver_UID)
+ var/mob/living/carbon/giver = attached_effect.owner
+ var/obj/item/I = locateUID(item_UID)
+ to_chat(giver, "You decide against giving [I] to [receiver].")
+ to_chat(receiver, "[giver] seems to have given up on giving you [I].")
+ receiver.clear_alert("take item [item_UID]") // This cancels *everything* related to the giving/item offering.
+
+
+/**
+ * # Give click intercept
+ *
+ * While a mob has this intercept, left clicking on a carbon mob will attempt to offer their currently held item to that mob.
+ */
+/datum/click_intercept/give
+ /// If the intercept user has succesfully offered the item to another player.
+ var/item_offered = FALSE
+
+/datum/click_intercept/give/New(client/C)
+ ..()
+ holder.mouse_pointer_icon = 'icons/mouse_icons/give_item.dmi'
+ to_chat(holder, "You can now left click on someone to give them your held item.")
+ RegisterSignal(holder.mob.get_active_hand(), list(COMSIG_PARENT_QDELETING, COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED), /datum/proc/signal_qdel)
+ RegisterSignal(holder.mob, list(SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), COMSIG_CARBON_SWAP_HANDS), /datum/proc/signal_qdel)
+
+
+/datum/click_intercept/give/Destroy(force = FALSE, ...)
+ holder.mouse_pointer_icon = initial(holder.mouse_pointer_icon)
+ if(!item_offered)
+ to_chat(holder.mob, "You're no longer trying to give someone your held item.")
+ return ..()
+
+
+/datum/click_intercept/give/InterceptClickOn(mob/user, params, atom/object)
+ if(user == object || !iscarbon(object))
+ return
+ var/mob/living/carbon/receiver = object
+ if(receiver.stat != CONSCIOUS)
+ to_chat(user, "[receiver] can't accept any items because they're not conscious!")
+ return
+ var/obj/item/I = user.get_active_hand()
+ if(!user.Adjacent(receiver))
+ to_chat(user, "You need to be closer to [receiver] to offer them [I].")
+ return
+ if(!receiver.client)
+ to_chat(user, "You offer [I] to [receiver], but they don't seem to respond...")
+ return
+ // We use UID() here so that the receiver can have more then one give request at one time.
+ // Otherwise, throwing a new "take item" alert would override any current one also named "take item".
+ receiver.throw_alert("take item [I.UID()]", /obj/screen/alert/take_item, alert_args = list(user, receiver, I))
+ item_offered = TRUE // TRUE so we don't give them the default chat message in Destroy.
+ to_chat(user, "You offer [I] to [receiver].")
+ qdel(src)
+
+
+/**
+ * # Take Item alert
+ *
+ * Alert which appears for a user when another player is attempting to offer them an item.
+ * The user can click the alert to accept, or simply do nothing to not take the item.
+ */
+/obj/screen/alert/take_item
+ name = "Take Item"
+ desc = "someone wants to hand you an item!"
+ icon_state = "template"
+ timeout = 10 SECONDS
+ /// UID of the mob offering the receiver an item.
+ var/giver_UID
+ /// UID of the mob who has this alert.
+ var/receiver_UID
+ /// UID of the item being given.
+ var/item_UID
+
+
+/obj/screen/alert/take_item/Initialize(mapload, mob/living/giver, mob/living/receiver, obj/item/I)
+ . = ..()
+ desc = "[giver] wants to hand you \a [I]. Click here to accept it!"
+ giver_UID = giver.UID()
+ receiver_UID = receiver.UID()
+ item_UID = I.UID()
+ giver.apply_status_effect(STATUS_EFFECT_OFFERING_ITEM, receiver_UID, item_UID)
+ add_overlay(icon(I.icon, I.icon_state, SOUTH))
+ add_overlay("alert_flash")
+ RegisterSignal(I, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED), .proc/cancel_give)
+ RegisterSignal(giver, list(SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), COMSIG_CARBON_SWAP_HANDS), .proc/cancel_give)
+ // If either of these atoms are deleted, we need to cancel everything. Also saves having to do null checks before interacting with these atoms.
+ RegisterSignal(I, COMSIG_PARENT_QDELETING, /datum/proc/signal_qdel)
+ RegisterSignal(giver, COMSIG_PARENT_QDELETING, /datum/proc/signal_qdel)
+
+
+/obj/screen/alert/take_item/Destroy()
+ var/mob/living/giver = locateUID(giver_UID)
+ giver.remove_status_effect(STATUS_EFFECT_OFFERING_ITEM)
+ return ..()
+
+
+/obj/screen/alert/take_item/proc/cancel_give()
+ SIGNAL_HANDLER
+ var/mob/living/giver = locateUID(giver_UID)
+ to_chat(giver, "You need to keep the item in your active hand if you want to hand it to someone!")
+ to_chat(locateUID(receiver_UID), "[giver] seems to have given up on giving you [locateUID(item_UID)].")
+ qdel(src)
+
+
+/obj/screen/alert/take_item/Click(location, control, params)
+ var/mob/living/receiver = locateUID(receiver_UID)
+ if(receiver.stat != CONSCIOUS)
+ return
+ var/obj/item/I = locateUID(item_UID)
+ if(HAS_TRAIT(receiver, TRAIT_HANDS_BLOCKED) || receiver.r_hand && receiver.l_hand)
+ to_chat(receiver, "You need to have your hands free to accept [I]!")
+ return
+ var/mob/living/giver = locateUID(giver_UID)
+ if(!giver.Adjacent(receiver))
+ to_chat(receiver, "You need to stay in reaching distance of [giver] to take [I]!")
+ return
+ if(I.flags & NODROP)
+ to_chat(giver, "[I] stays stuck to your hand when [receiver] tries to take it!")
+ to_chat(receiver, "[I] stays stuck to [giver]'s hand when you try to take it!")
+ return
+ giver.unEquip(I)
+ receiver.put_in_hands(I)
+ I.add_fingerprint(receiver)
+ I.on_give(giver, receiver)
+ receiver.visible_message("[giver] handed [I] to [receiver].")
+ receiver.clear_alert("give item [item_UID]")
+
+
+/obj/screen/alert/take_item/do_timeout(mob/M, category)
+ var/mob/living/giver = locateUID(giver_UID)
+ var/mob/living/receiver = locateUID(receiver_UID)
+ // Make sure we're still nearby. We don't want to show a message if the giver not near us.
+ if(giver in view(3, receiver))
+ var/obj/item/I = locateUID(item_UID)
+ to_chat(giver, "You tried to hand [I] to [receiver], but they didn't want it.")
+ to_chat(receiver, "[giver] seems to have given up on giving you [I].")
+ ..()
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index d6ba5a8e59e..5b03e5959ed 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -9,6 +9,8 @@
mobspellremove(src)
QDEL_LIST(viruses)
QDEL_LIST(actions)
+ for(var/alert in alerts)
+ clear_alert(alert)
ghostize()
QDEL_LIST_ASSOC_VAL(tkgrabbed_objects)
for(var/I in tkgrabbed_objects)
diff --git a/icons/mob/screen_alert.dmi b/icons/mob/screen_alert.dmi
index 5b65a8175f1..4ef228dcd80 100644
Binary files a/icons/mob/screen_alert.dmi and b/icons/mob/screen_alert.dmi differ
diff --git a/icons/mouse_icons/give_item.dmi b/icons/mouse_icons/give_item.dmi
new file mode 100644
index 00000000000..6b4944261a6
Binary files /dev/null and b/icons/mouse_icons/give_item.dmi differ