diff --git a/code/__DEFINES/memory_defines.dm b/code/__DEFINES/memory_defines.dm
index 30de201dc58..77c6f151ef7 100644
--- a/code/__DEFINES/memory_defines.dm
+++ b/code/__DEFINES/memory_defines.dm
@@ -60,6 +60,8 @@
#define MEMORY_BOMB_PRIMED "bomb"
/// A memory of pulling off either a high five or a high ten
#define MEMORY_HIGH_FIVE "highfive"
+/// A memory of being elegantly helped up!
+#define MEMORY_HELPED_UP "helped_up"
/// A memory of getting borged
#define MEMORY_BORGED "borged"
/// A memory of dying! includes time of death
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index c441690962f..9d0cbc60b08 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -302,6 +302,8 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
icon_state = "default"
var/mob/living/carbon/offerer
var/obj/item/receiving
+ /// Additional text displayed in the description of the alert.
+ var/additional_desc_text = "Click this alert to take it."
/atom/movable/screen/alert/give/Destroy()
offerer = null
@@ -312,21 +314,42 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
* Handles assigning most of the variables for the alert that pops up when an item is offered
*
* Handles setting the name, description and icon of the alert and tracking the person giving
- * and the item being offered, also registers a signal that removes the alert from anyone who moves away from the offerer
+ * and the item being offered.
* Arguments:
* * taker - The person receiving the alert
* * offerer - The person giving the alert and item
* * receiving - The item being given by the offerer
*/
/atom/movable/screen/alert/give/proc/setup(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
- name = "[offerer] is offering [receiving]"
- desc = "[offerer] is offering [receiving]. Click this alert to take it."
+ var/receiving_name = get_receiving_name(taker, offerer, receiving)
+ name = "[offerer] is offering [receiving_name]"
+ desc = "[offerer] is offering [receiving_name]. [additional_desc_text]"
icon_state = "template"
cut_overlays()
add_overlay(receiving)
src.receiving = receiving
src.offerer = offerer
+
+/**
+ * Called right before `setup()`, to do any sort of logic to change the name of
+ * what's displayed as the name of what's being offered in the alert. Use this to
+ * add pronouns and the like, or to totally override the displayed name!
+ * Also the best place to make changes to `additional_desc_text` before `setup()`
+ * without having to override `setup()` entirely.
+ *
+ * Arguments:
+ * * taker - The person receiving the alert
+ * * offerer - The person giving the alert and item
+ * * receiving - The item being given by the offerer
+ *
+ * Returns a string that will be displayed in the alert, which is `receiving.name`
+ * by default.
+ */
+/atom/movable/screen/alert/give/proc/get_receiving_name(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
+ return receiving.name
+
+
/atom/movable/screen/alert/give/Click(location, control, params)
. = ..()
if(!.)
@@ -343,12 +366,20 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
taker.take(offerer, receiving)
SEND_SIGNAL(offerer, COMSIG_CARBON_ITEM_GIVEN, taker, receiving)
+
+/atom/movable/screen/alert/give/highfive
+ additional_desc_text = "Click this alert to slap it."
+
+
+/atom/movable/screen/alert/give/highfive/get_receiving_name(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
+ return "a high-five"
+
+
/atom/movable/screen/alert/give/highfive/setup(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
. = ..()
- name = "[offerer] is offering a high-five!"
- desc = "[offerer] is offering a high-five! Click this alert to slap it."
RegisterSignal(offerer, COMSIG_PARENT_EXAMINE_MORE, PROC_REF(check_fake_out))
+
/atom/movable/screen/alert/give/highfive/handle_transfer()
var/mob/living/carbon/taker = owner
if(receiving && (receiving in offerer.held_items))
@@ -391,6 +422,17 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
if(!receiving)
examine_list += "[span_warning("[offerer]'s arm appears tensed up, as if [offerer.p_they()] plan on pulling it back suddenly...")]\n"
+
+/atom/movable/screen/alert/give/hand/get_receiving_name(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
+ additional_desc_text = "Click this alert to take it and let [offerer.p_them()] pull you around!"
+ return "[offerer.p_their()] [receiving]"
+
+
+/atom/movable/screen/alert/give/hand/helping/get_receiving_name(mob/living/carbon/taker, mob/living/carbon/offerer, obj/item/receiving)
+ . = ..()
+ additional_desc_text = "Click this alert to let them help you up!"
+
+
/atom/movable/screen/alert/give/secret_handshake
icon_state = "default"
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index 06bd8209f94..7cd06ea3855 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -211,6 +211,20 @@
mood_change = 2
timeout = 45 SECONDS
+/datum/mood_event/helped_up
+ description = "Helping them up felt good!"
+ mood_change = 2
+ timeout = 45 SECONDS
+
+/datum/mood_event/helped_up/add_effects(mob/other_person, helper)
+ if(!other_person)
+ return
+
+ if(helper)
+ description = "Helping [other_person] up felt good!"
+ else
+ description = "[other_person] helped me up, how nice of [other_person.p_them()]!"
+
/datum/mood_event/high_ten
description = "AMAZING! A HIGH-TEN!"
mood_change = 3
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 312346ebcab..3fe5d94e548 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -171,12 +171,13 @@
if(give_alert_override)
give_alert_type = give_alert_override
- if(offered && owner.CanReach(offered) && !IS_DEAD_OR_INCAP(offered) && offered.can_hold_items())
+ if(offered && is_taker_elligible(offered))
register_candidate(offered)
else
for(var/mob/living/carbon/possible_taker in orange(1, owner))
- if(!owner.CanReach(possible_taker) || IS_DEAD_OR_INCAP(possible_taker) || !possible_taker.can_hold_items())
+ if(!is_taker_elligible(possible_taker))
continue
+
register_candidate(possible_taker)
if(!possible_takers) // no one around
@@ -234,6 +235,87 @@
SIGNAL_HANDLER
qdel(src)
+/**
+ * Is our taker valid as a target for the offering? Meant to be used when registering
+ * takers in `on_creation()`. You should override `additional_taker_check()` instead of this.
+ *
+ * Returns `TRUE` if the taker is valid as a target for the offering.
+ */
+/datum/status_effect/offering/proc/is_taker_elligible(mob/living/carbon/taker)
+ return owner.CanReach(taker) && !IS_DEAD_OR_INCAP(taker) && additional_taker_check(taker)
+
+
+/**
+ * Additional checks added to `CanReach()` and `IS_DEAD_OR_INCAP()` in `is_taker_elligible()`.
+ * Should be what you override instead of `is_taker_elligible()`. By default, checks if the
+ * taker can hold items.
+ *
+ * Returns `TRUE` if the taker is valid as a target for the offering based on these
+ * additional checks.
+ */
+/datum/status_effect/offering/proc/additional_taker_check(mob/living/carbon/taker)
+ return taker.can_hold_items()
+
+
+/**
+ * This status effect is meant only for items that you don't actually receive
+ * when offered, mostly useful for `/obj/item/hand_item` subtypes.
+ */
+/datum/status_effect/offering/no_item_received
+
+
+/datum/status_effect/offering/no_item_received/additional_taker_check(mob/living/carbon/taker)
+ return TRUE
+
+
+/**
+ * This status effect is meant only to be used for offerings that require the target to
+ * be resting (like when you're trying to give them a hand to help them up).
+ * Also doesn't require them to have their hands free (since you're not giving them
+ * anything).
+ */
+/datum/status_effect/offering/no_item_received/needs_resting
+
+
+/datum/status_effect/offering/no_item_received/needs_resting/additional_taker_check(mob/living/carbon/taker)
+ return taker.body_position == LYING_DOWN
+
+
+/datum/status_effect/offering/no_item_received/needs_resting/on_creation(mob/living/new_owner, obj/item/offer, give_alert_override, mob/living/carbon/offered)
+ . = ..()
+ RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(check_owner_standing))
+
+
+/datum/status_effect/offering/no_item_received/needs_resting/register_candidate(mob/living/carbon/possible_candidate)
+ . = ..()
+ RegisterSignal(possible_candidate, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(check_candidate_resting))
+
+
+/datum/status_effect/offering/no_item_received/needs_resting/remove_candidate(mob/living/carbon/removed_candidate)
+ UnregisterSignal(removed_candidate, COMSIG_LIVING_SET_BODY_POSITION)
+ return ..()
+
+
+/// Simple signal handler that ensures that, if the owner stops standing, the offer no longer stands either!
+/datum/status_effect/offering/no_item_received/needs_resting/proc/check_owner_standing(mob/living/carbon/owner)
+ if(src.owner.body_position == STANDING_UP)
+ return
+
+ // This doesn't work anymore if the owner is no longer standing up, sorry!
+ qdel(src)
+
+
+/// Simple signal handler that ensures that, should a candidate now be standing up, the offer won't be standing for them anymore!
+/datum/status_effect/offering/no_item_received/needs_resting/proc/check_candidate_resting(mob/living/carbon/candidate)
+ SIGNAL_HANDLER
+
+ if(candidate.body_position == LYING_DOWN)
+ return
+
+ // No longer lying down? You're no longer eligible to take the offer, sorry!
+ remove_candidate(candidate)
+
+
/datum/status_effect/offering/secret_handshake
id = "secret_handshake"
give_alert_type = /atom/movable/screen/alert/give/secret_handshake
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index bfb2b93a3e8..824e2eac4f7 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1330,9 +1330,10 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
* * Return TRUE if you want to interrupt the offer.
*
* * Arguments:
- * * offerer - the person offering the item
+ * * offerer - The person offering the item.
+ * * offered - The person being offered the item.
*/
-/obj/item/proc/on_offered(mob/living/carbon/offerer)
+/obj/item/proc/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
if(SEND_SIGNAL(src, COMSIG_ITEM_OFFERING, offerer) & COMPONENT_OFFER_INTERRUPT)
return TRUE
diff --git a/code/game/objects/items/hand_items.dm b/code/game/objects/items/hand_items.dm
index 37939e70b1f..55576bfc609 100644
--- a/code/game/objects/items/hand_items.dm
+++ b/code/game/objects/items/hand_items.dm
@@ -313,7 +313,7 @@
user.visible_message("[span_danger("[user] slams [user.p_their()] fist down on [table]!")]", "[span_danger("You slam your fist down on [table]!")]")
qdel(src)
-/obj/item/hand_item/slapper/on_offered(mob/living/carbon/offerer)
+/obj/item/hand_item/slapper/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
. = TRUE
if(!(locate(/mob/living/carbon) in orange(1, offerer)))
@@ -363,6 +363,109 @@
taker.add_mood_event("high_five", /datum/mood_event/high_five)
qdel(src)
+
+/obj/item/hand_item/hand
+ name = "hand"
+ desc = "Sometimes, you just want to act gentlemanly."
+ icon_state = "latexballon"
+ inhand_icon_state = "nothing"
+
+
+/obj/item/hand_item/hand/pre_attack(mob/living/carbon/help_target, mob/living/carbon/helper, params)
+ if(!loc.Adjacent(help_target) || !istype(helper) || !istype(help_target))
+ return ..()
+
+ if(helper.resting)
+ to_chat(helper, span_warning("You can't act gentlemanly when you're lying down!"))
+ return TRUE
+
+
+/obj/item/hand_item/hand/pre_attack_secondary(mob/living/carbon/help_target, mob/living/carbon/helper, params)
+ if(!loc.Adjacent(help_target) || !istype(helper) || !istype(help_target))
+ return ..()
+
+ if(helper.resting)
+ to_chat(helper, span_warning("You can't act gentlemanly when you're lying down!"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ return SECONDARY_ATTACK_CALL_NORMAL
+
+
+/obj/item/hand_item/hand/attack(mob/living/carbon/target_mob, mob/living/carbon/user, params)
+ if(!loc.Adjacent(target_mob) || !istype(user) || !istype(target_mob))
+ return TRUE
+
+ user.give(target_mob)
+ return TRUE
+
+
+/obj/item/hand_item/hand/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
+ . = TRUE
+
+ if(!istype(offerer))
+ return
+
+ if(offerer.body_position == LYING_DOWN)
+ to_chat(offerer, span_warning("You can't act gentlemanly when you're lying down!"))
+ return
+
+ if(!offered)
+ offered = locate(/mob/living/carbon) in orange(1, offerer)
+
+ if(offered && istype(offered) && offered.body_position == LYING_DOWN)
+ offerer.visible_message(span_notice("[offerer] offers [offerer.p_their()] hand to [offered], looking to help them up!"),
+ span_notice("You offer [offered] your hand, to try to help them up!"), null, 2)
+
+ offerer.apply_status_effect(/datum/status_effect/offering/no_item_received/needs_resting, src, /atom/movable/screen/alert/give/hand/helping, offered)
+ return
+
+ offerer.visible_message(span_notice("[offerer] extends out [offerer.p_their()] hand."),
+ span_notice("You extend out your hand."), null, 2)
+
+ offerer.apply_status_effect(/datum/status_effect/offering/no_item_received, src, /atom/movable/screen/alert/give/hand)
+ return
+
+
+/obj/item/hand_item/hand/on_offer_taken(mob/living/carbon/offerer, mob/living/carbon/taker)
+ . = TRUE
+
+ if(taker.body_position == LYING_DOWN)
+ taker.help_shake_act(offerer)
+
+ if(taker.body_position == LYING_DOWN)
+ return // That didn't help them. Awkwaaaaard.
+
+ offerer.visible_message(span_notice("[offerer] helps [taker] up!"), span_nicegreen("You help [taker] up!"), span_hear("You hear someone helping someone else up!"), ignored_mobs = taker)
+ to_chat(taker, span_nicegreen("You take [offerer]'s hand, letting [offerer.p_them()] help your up! How nice of them!"))
+
+ offerer.mind.add_memory(MEMORY_HELPED_UP, list(DETAIL_DEUTERAGONIST = taker), story_value = STORY_VALUE_OKAY)
+ taker.mind.add_memory(MEMORY_HELPED_UP, list(DETAIL_DEUTERAGONIST = offerer), story_value = STORY_VALUE_OKAY)
+
+ offerer.add_mood_event("helping_up", /datum/mood_event/helped_up, taker, TRUE) // Different IDs because you could be helped up and then help someone else up.
+ taker.add_mood_event("helped_up", /datum/mood_event/helped_up, offerer, FALSE)
+
+ qdel(src)
+ return
+
+ if(taker.buckled?.buckle_prevents_pull)
+ return // Can't start pulling them if they're buckled and that prevents pulls.
+
+ // We do a little switcheroo to ensure that it displays the pulling message that mentions
+ // taking taker by their hands.
+ var/offerer_zone_selected = offerer.zone_selected
+ offerer.zone_selected = "r_arm"
+ var/did_we_pull = offerer.start_pulling(taker) // Will return either null or FALSE. We only want to silence FALSE.
+ offerer.zone_selected = offerer_zone_selected
+
+ if(did_we_pull == FALSE)
+ return // That didn't work for one reason or the other. No need to display anything.
+
+ to_chat(offerer, span_notice("[taker] takes your hand, allowing you to pull [taker.p_them()] along."))
+ to_chat(taker, span_notice("You take [offerer]'s hand, which allows [offerer.p_them()] to pull you along. How polite!"))
+
+ qdel(src)
+
+
/obj/item/hand_item/stealer
name = "steal"
desc = "Your filthy little fingers are ready to commit crimes."
@@ -427,14 +530,14 @@
blown_kiss.fire()
qdel(src)
-/obj/item/hand_item/kisser/on_offered(mob/living/carbon/offerer)
+/obj/item/hand_item/kisser/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
if(!(locate(/mob/living/carbon) in orange(1, offerer)))
return TRUE
cheek_kiss = (offerer.zone_selected != BODY_ZONE_PRECISE_MOUTH)
offerer.visible_message(span_notice("[offerer] leans in slightly, offering a kiss[cheek_kiss ? " on the cheek" : ""]!"),
span_notice("You lean in slightly, indicating you'd like to offer a kiss[cheek_kiss ? " on the cheek" : ""]!"), null, 2)
- offerer.apply_status_effect(/datum/status_effect/offering, src)
+ offerer.apply_status_effect(/datum/status_effect/offering/no_item_received, src)
return TRUE
/obj/item/hand_item/kisser/on_offer_taken(mob/living/carbon/offerer, mob/living/carbon/taker)
diff --git a/code/modules/mob/living/carbon/emote.dm b/code/modules/mob/living/carbon/emote.dm
index 3b6e962115a..bc9384de23b 100644
--- a/code/modules/mob/living/carbon/emote.dm
+++ b/code/modules/mob/living/carbon/emote.dm
@@ -134,6 +134,26 @@
qdel(N)
to_chat(user, span_warning("You're incapable of slapping in your current state."))
+
+/datum/emote/living/carbon/hand
+ key = "hand"
+ key_third_person = "hands"
+ hands_use_check = TRUE
+
+
+/datum/emote/living/carbon/hand/run_emote(mob/user, params, type_override, intentional)
+ . = ..()
+ if(!.)
+ return
+
+ var/obj/item/hand_item/hand/hand = new(user)
+ if(user.put_in_hands(hand))
+ to_chat(user, span_notice("You ready your hand."))
+ else
+ qdel(hand)
+ to_chat(user, span_warning("You're incapable of using your hand in your current state."))
+
+
/datum/emote/living/carbon/snap
key = "snap"
key_third_person = "snaps"
diff --git a/strings/memories.json b/strings/memories.json
index 7ebbe0b4d87..c09ff61cbc7 100644
--- a/strings/memories.json
+++ b/strings/memories.json
@@ -191,6 +191,13 @@
"What a %HIGHFIVE_TYPE! %PROTAGONIST %MOOD.",
"Wow! %PROTAGONIST %MOOD!"
],
+ "helped_up_names":[
+ "%PROTAGONIST gentlemanly helping up %DEUTERAGONIST."
+ ],
+ "helped_up_starts":[
+ "%PROTAGONIST helping up %DEUTERAGONIST",
+ "%DEUTERAGONIST taking the hand offered graciously by %PROTAGONIST to get up"
+ ],
"borged_names":[
"The borging of %PROTAGONIST."
],