diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 52fbb0c7b85..b099f50c431 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -994,6 +994,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SKIP_BASIC_REACH_CHECK "skip_basic_reach_check"
/// Increases chance of this brain getting special traumas, makes them harder to cure
#define TRAIT_SPECIAL_TRAUMA_BOOST "special_trauma_boost"
+/// If this item is offered when something is pulled.
+#define TRAIT_OFFERED_WHEN_PULLED "offered_when_pulled"
+/// If the item has a special mob/living/give() interaction.
+#define TRAIT_BORG_GIVE "give_borg_item"
//---- Heretic Traits
/// Hides the heretic overlay that outs them as the heretic
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 6dde06107ba..55ad2b7728b 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -714,6 +714,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NODROP" = TRAIT_NODROP,
"TRAIT_NO_WORN_ICON" = TRAIT_NO_WORN_ICON,
"TRAIT_NULLROD_ITEM" = TRAIT_NULLROD_ITEM,
+ "TRAIT_OFFERED_WHEN_PULLED" = TRAIT_OFFERED_WHEN_PULLED,
"TRAIT_OMNI_BAIT" = TRAIT_OMNI_BAIT,
"TRAIT_PLANT_WILDMUTATE" = TRAIT_PLANT_WILDMUTATE,
"TRAIT_POISONOUS_BAIT" = TRAIT_POISONOUS_BAIT,
@@ -738,6 +739,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
/obj/item/bodypart/head = list(
"TRAIT_DISFIGURED" = TRAIT_DISFIGURED,
),
+ /obj/item/borg = list(
+ "TRAIT_BORG_GIVE" = TRAIT_BORG_GIVE,
+ ),
/obj/item/card/id = list(
"TRAIT_JOB_FIRST_ID_CARD" = TRAIT_JOB_FIRST_ID_CARD,
"TRAIT_MAGNETIC_ID_CARD" = TRAIT_MAGNETIC_ID_CARD,
diff --git a/code/_onclick/hud/screen_objects/alert.dm b/code/_onclick/hud/screen_objects/alert.dm
index af7de7ee085..13272c9c407 100644
--- a/code/_onclick/hud/screen_objects/alert.dm
+++ b/code/_onclick/hud/screen_objects/alert.dm
@@ -410,6 +410,8 @@
var/screentip_override_text
/// Whether the offered item can be examined by shift-clicking the alert
var/examinable = TRUE
+ /// Whether this item should bypass active hand checks.
+ var/bypass_active_hand = FALSE
/atom/movable/screen/alert/give/Initialize(mapload, datum/hud/hud_owner)
. = ..()
@@ -488,9 +490,13 @@
var/mob/living/taker = owner
var/mob/living/offerer = offer.owner
var/obj/item/receiving = offer.offered_item
- taker.take(offerer, receiving)
+ taker.take(offerer, receiving, bypass_active_hand)
SEND_SIGNAL(offerer, COMSIG_LIVING_ITEM_GIVEN, taker, receiving)
+/// Mostly for borgs to offer items.
+/atom/movable/screen/alert/give/borg
+ bypass_active_hand = TRUE
+
/atom/movable/screen/alert/give/highfive
additional_desc_text = "Click this alert to slap it."
screentip_override_text = "High Five"
diff --git a/code/datums/beam.dm b/code/datums/beam.dm
index b3debf2de38..e690536a15c 100644
--- a/code/datums/beam.dm
+++ b/code/datums/beam.dm
@@ -512,6 +512,68 @@
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(owner, COMSIG_BEAM_TURFS_CHANGED, post_change_callbacks)
+/// For beams that might be from a held item.
+/datum/beam/held
+ // Is the fishing rod held in left side hand
+ var/lefthand = FALSE
+
+ // Make these inline with final sprites
+ var/righthand_s_px = 13
+ var/righthand_s_py = 16
+
+ var/righthand_e_px = 18
+ var/righthand_e_py = 16
+
+ var/righthand_w_px = -20
+ var/righthand_w_py = 18
+
+ var/righthand_n_px = -14
+ var/righthand_n_py = 16
+
+ var/lefthand_s_px = -13
+ var/lefthand_s_py = 15
+
+ var/lefthand_e_px = 24
+ var/lefthand_e_py = 18
+
+ var/lefthand_w_px = -17
+ var/lefthand_w_py = 16
+
+ var/lefthand_n_px = 13
+ var/lefthand_n_py = 15
+
+/datum/beam/held/Start()
+ update_offsets(origin.dir)
+ . = ..()
+ RegisterSignal(origin, COMSIG_ATOM_DIR_CHANGE, PROC_REF(handle_dir_change))
+
+/datum/beam/held/Destroy()
+ UnregisterSignal(origin, COMSIG_ATOM_DIR_CHANGE)
+ . = ..()
+
+/datum/beam/held/proc/handle_dir_change(atom/movable/source, olddir, newdir)
+ SIGNAL_HANDLER
+ update_offsets(newdir)
+ INVOKE_ASYNC(src, TYPE_PROC_REF(/datum/beam, redrawing))
+
+/datum/beam/held/proc/update_offsets(user_dir)
+ switch(user_dir)
+ if(SOUTH)
+ override_origin_pixel_x = lefthand ? lefthand_s_px : righthand_s_px
+ override_origin_pixel_y = lefthand ? lefthand_s_py : righthand_s_py
+ if(EAST)
+ override_origin_pixel_x = lefthand ? lefthand_e_px : righthand_e_px
+ override_origin_pixel_y = lefthand ? lefthand_e_py : righthand_e_py
+ if(WEST)
+ override_origin_pixel_x = lefthand ? lefthand_w_px : righthand_w_px
+ override_origin_pixel_y = lefthand ? lefthand_w_py : righthand_w_py
+ if(NORTH)
+ override_origin_pixel_x = lefthand ? lefthand_n_px : righthand_n_px
+ override_origin_pixel_y = lefthand ? lefthand_n_py : righthand_n_py
+
+ override_origin_pixel_x += origin.pixel_x
+ override_origin_pixel_y += origin.pixel_y
+
/**
* This is what you use to start a beam. Example: origin.Beam(target, args). **Store the return of this proc if you don't set maxdist or time, you need it to delete the beam.**
*
diff --git a/code/datums/components/offered_when_pulled.dm b/code/datums/components/offered_when_pulled.dm
new file mode 100644
index 00000000000..78ee7e97584
--- /dev/null
+++ b/code/datums/components/offered_when_pulled.dm
@@ -0,0 +1,24 @@
+/**For items automatically offered to a player when they pull a borg.*/
+
+/datum/component/borg_item_offered_when_pulled
+ var/mob/living/silicon/robot
+
+/datum/component/borg_item_offered_when_pulled/Initialize(mob/living/silicon/robot/borg)
+ . = ..()
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+ if(!istype(borg))
+ stack_trace("Component assigned to [parent], but was not assigned a ref for type /mob/living/silicon/robot to attach COMSIG_LIVING_GET_PULLED")
+ return COMPONENT_INCOMPATIBLE
+ RegisterSignal(borg, COMSIG_LIVING_GET_PULLED, PROC_REF(on_pulled))
+ ADD_TRAIT(parent, TRAIT_OFFERED_WHEN_PULLED, TRAIT_OFFERED_WHEN_PULLED)
+ robot = borg
+
+/datum/component/borg_item_offered_when_pulled/Destroy(datum/source, ...)
+ UnregisterSignal(robot, COMSIG_LIVING_GET_PULLED)
+ robot = null
+ return ..()
+
+/datum/component/borg_item_offered_when_pulled/proc/on_pulled(mob/living/holder, mob/living/puller)
+ SIGNAL_HANDLER
+ holder.give(puller, parent)
diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm
index 33d24177b54..34b74388aff 100644
--- a/code/datums/keybinding/living.dm
+++ b/code/datums/keybinding/living.dm
@@ -215,6 +215,11 @@
return FALSE
if(!user.mob)
return FALSE
+ var/obj/item/object = user.mob.get_active_held_item()
+ if(isnull(object))
+ return FALSE
+ if(HAS_TRAIT(object, TRAIT_BORG_GIVE))
+ return TRUE
if(!HAS_TRAIT(user.mob, TRAIT_CAN_HOLD_ITEMS))
return FALSE
return TRUE
@@ -224,7 +229,10 @@
if(.)
return
var/mob/living/living_user = user.mob
- if(!HAS_TRAIT(living_user, TRAIT_CAN_HOLD_ITEMS))
+ var/obj/item/held_item = living_user.get_active_held_item()
+ if(isnull(held_item))
+ return
+ if(!HAS_TRAIT(living_user, TRAIT_CAN_HOLD_ITEMS) && !HAS_TRAIT(held_item, TRAIT_BORG_GIVE))
return
living_user.give()
diff --git a/code/datums/status_effects/debuffs/hooked.dm b/code/datums/status_effects/debuffs/hooked.dm
index 4747941ca00..d6dfa473c64 100644
--- a/code/datums/status_effects/debuffs/hooked.dm
+++ b/code/datums/status_effects/debuffs/hooked.dm
@@ -12,7 +12,7 @@
/datum/status_effect/grouped/hooked/proc/still_exists()
return !QDELETED(src)
-/datum/status_effect/grouped/hooked/source_added(datum/beam/fishing_line/source)
+/datum/status_effect/grouped/hooked/source_added(datum/beam/held/source)
RegisterSignal(source, COMSIG_QDELETING, PROC_REF(on_fishing_line_deleted))
/datum/status_effect/grouped/hooked/proc/on_fishing_line_deleted(datum/source)
@@ -37,7 +37,7 @@
if(!effect.try_unhook())
return
owner.balloon_alert(owner, "hook removed")
- var/datum/beam/fishing_line/rand_source = pick(effect.sources)
+ var/datum/beam/held/rand_source = pick(effect.sources)
qdel(rand_source)
///Version used by the jawed fishing hook, which also applies slowdown
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 9ccc6e3d333..4b787d51b82 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -214,6 +214,8 @@
var/list/possible_takers
/// The actual item being offered
var/obj/item/offered_item
+ ///If we should bypass active_hand checks.
+ var/bypass_active_hand = FALSE
/// The type of alert given to people when offered, in case you need to override some behavior (like for high-fives)
var/give_alert_type = /atom/movable/screen/alert/give
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index c8438dc81dc..296dc4e237b 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1497,7 +1497,7 @@ GAME_VERB_SRC(/obj/item, verb_pickup, oview(1), "Pick up", null)
* * taker - the living mob trying to accept the offer
*/
/obj/item/proc/on_offer_taken(mob/living/offerer, mob/living/taker)
- if(!(HAS_TRAIT(offerer, TRAIT_CAN_HOLD_ITEMS) && HAS_TRAIT(taker, TRAIT_CAN_HOLD_ITEMS)))
+ if(!HAS_TRAIT(offerer, TRAIT_CAN_HOLD_ITEMS) && !HAS_TRAIT(src, TRAIT_BORG_GIVE) && HAS_TRAIT(taker, TRAIT_CAN_HOLD_ITEMS))
return TRUE // both must be able to hold items for this to make sense
if(SEND_SIGNAL(src, COMSIG_ITEM_OFFER_TAKEN, offerer, taker) & COMPONENT_OFFER_INTERRUPT)
return TRUE
diff --git a/code/game/objects/items/robot/items/janitor.dm b/code/game/objects/items/robot/items/janitor.dm
new file mode 100644
index 00000000000..75f47f70f27
--- /dev/null
+++ b/code/game/objects/items/robot/items/janitor.dm
@@ -0,0 +1,243 @@
+/obj/item/borg/cleaner_box
+ name = "janitorial vacuum suite"
+ desc = "A module designed to compensate for your lack of hands by offloading your job onto your more squishy overlords."
+ icon = 'icons/obj/items_cyborg.dmi'
+ icon_state = "cleanerbox"
+ var/obj/item/vacuum_item/hose
+ var/deployed = FALSE
+ var/locked = FALSE
+ var/datum/weakref/module_list
+
+/obj/item/borg/cleaner_box/Initialize(mapload)
+ . = ..()
+ var/mob/living/silicon/robot = loc
+ if(!istype(robot))
+ return INITIALIZE_HINT_QDEL
+ var/obj/item/robot_model/janitor/model = locate() in robot.get_contents()
+ module_list = WEAKREF(model)
+ AddComponent(/datum/component/borg_item_offered_when_pulled, robot)
+ ADD_TRAIT(src, TRAIT_BORG_GIVE, TRAIT_BORG_GIVE)
+ hose = new(src)
+ hose.cleaner_box = WEAKREF(src)
+ hose.AddComponent( \
+ /datum/component/transforming, \
+ force_on = hose.force, \
+ hitsound_on = hose.hitsound, \
+ w_class_on = hose.w_class, \
+ clumsy_check = FALSE, \
+ attack_verb_continuous_on = list("washed", "mopped", "scrubbed", "whacked", "bapped", "decontaminated"), \
+ attack_verb_simple_on = list("wash", "mop", "scrub", "whack", "bap", "decontaminate"), \
+ )
+ hose.RegisterSignal(hose, COMSIG_TRANSFORMING_ON_TRANSFORM, TYPE_PROC_REF(/obj/item/vacuum_item, on_transform))
+ update_icon(UPDATE_OVERLAYS)
+
+/obj/item/borg/cleaner_box/Destroy(force)
+ if(hose?.borg_hose)
+ QDEL_NULL(hose.borg_hose)
+ if(deployed)
+ hose.retract_hose()
+ QDEL_NULL(hose)
+ return ..()
+
+/obj/item/borg/cleaner_box/attack_self(mob/user, modifiers)
+ . = ..()
+ if(deployed)
+ hose.retract_hose()
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/obj/item/borg/cleaner_box/click_alt(mob/user)
+ if(deployed)
+ hose.retract_hose()
+ locked = !locked
+ update_icon(UPDATE_OVERLAYS)
+ return CLICK_ACTION_SUCCESS
+
+/obj/item/borg/cleaner_box/on_offered(mob/living/offerer, mob/living/carbon/offered)
+ . = TRUE
+ if(SEND_SIGNAL(src, COMSIG_ITEM_OFFERING, offerer) & COMPONENT_OFFER_INTERRUPT)
+ return
+ if(hose.loc != src && !istype(hose.loc, /mob/living)) // Error handling
+ stack_trace("[src] has been offered with [hose] not present inside its contents or inside a mob's loc variable. Location: [isnull(loc) ? "NULLSPACE" : "[hose.loc], X: [hose.x], Y: [hose.y], Z:[hose.z]"]")
+ deployed = FALSE
+ hose.forceMove(src)
+ if(locked || deployed)
+ return
+ if(!offered)
+ offered = locate(/mob/living/carbon) in orange(1, offerer)
+ if(offered && istype(offered))
+ offerer.visible_message(
+ span_notice("[offerer] extends the handle towards [offered] for their cleaning suite."),
+ span_notice("The handle to your [src] extends towards [offered]'s hand."), null, 2)
+
+ offerer.apply_status_effect(/datum/status_effect/offering, src, /atom/movable/screen/alert/give/borg, offered)
+ return
+
+/obj/item/borg/cleaner_box/on_offer_taken(mob/living/offerer, mob/living/taker)
+ . = ..()
+ if(.)
+ return TRUE
+ hose.bag = istype(loc, /mob/living/silicon) ? pick(loc.get_all_contents_type(/obj/item/storage/bag/trash)) : locate(/obj/item/storage/bag/trash) in module_list.resolve()
+ if(!hose.bag)
+ stack_trace("[src] failed to connect to a trash bag on [module_list.resolve()].")
+ return TRUE
+ taker.visible_message(
+ span_notice("[taker] takes the [hose] from [offerer]."),
+ span_notice("You take the [hose] from [offerer]"))
+ hose.do_pickup_animation(taker, offerer)
+ taker.put_in_hands(hose)
+ hose.borg_hose = hose.generate_hose(offerer, taker)
+ hose.RegisterSignal(hose, COMSIG_ITEM_DROPPED, TYPE_PROC_REF(/obj/item/vacuum_item, on_drop))
+ playsound(hose, 'sound/items/vacuum/vacuum_hose.ogg', 100, TRUE)
+ deployed = TRUE
+ update_icon(UPDATE_OVERLAYS)
+ offerer.remove_status_effect(/datum/status_effect/offering)
+ return TRUE
+
+/obj/item/borg/cleaner_box/update_overlays()
+ . = ..()
+ if(deployed)
+ . += "cleanerbox_on"
+ else
+ . += "cleanerbox_wand"
+ if(locked)
+ . += "cleanerbox_locked"
+
+/obj/item/borg/cleaner_box/examine(mob/user)
+ . = ..()
+ . += span_notice("Alt-Click to [locked ? "unlock" : "lock"] the [src].")
+
+/obj/item/vacuum_item
+ name = "janitorial floor cleaner"
+ desc = "This is the working end of an industrial cleaner that someone unfortinately gave sapience."
+ icon = 'icons/obj/items_cyborg.dmi'
+ icon_state = "vacuum-wand"
+ inhand_icon_state = "vacuum-wand"
+ righthand_file = 'icons/mob/inhands/items/vacuum_wand_righthand.dmi'
+ lefthand_file = 'icons/mob/inhands/items/vacuum_wand_lefthand.dmi'
+ w_class = WEIGHT_CLASS_BULKY
+ obj_flags = INDESTRUCTIBLE // To prevent fuckery and a broken borg module.
+ attack_verb_continuous = list("sucked", "vacuumed", "smacked", "forcefully dusted off", "beaten")
+ attack_verb_simple = list("suck", "vacuum", "smack", "dust off", "beat")
+ force = 12
+
+ var/datum/beam/held/vacuum/borg_hose
+ var/datum/weakref/cleaner_box
+ var/obj/item/storage/bag/trash/bag
+ var/cleaning = FALSE
+
+/obj/item/vacuum_item/Destroy(force)
+ bag = null
+ return ..()
+
+/obj/item/vacuum_item/interact_with_atom(obj/item/thing, mob/living/user, list/modifiers)
+ . = ..()
+ if(!istype(thing))
+ return NONE
+ if(!bag && cleaning)
+ return NONE
+ if(thing.anchored || thing.w_class >= WEIGHT_CLASS_BULKY)
+ return NONE
+ playsound(bag, 'sound/items/vacuum/vacuum_use.ogg', 20, TRUE)
+ for(var/obj/item/I in get_turf(thing))
+ if(!istype(I, thing.type))
+ continue
+ if(!do_after(user, 0.1 SECONDS, user, progress = FALSE))
+ break
+ if(bag.atom_storage.attempt_insert(I, user, FALSE))
+ continue
+ break
+
+/obj/item/vacuum_item/proc/on_transform(obj/item/source, mob/living/user, active)
+ SIGNAL_HANDLER
+
+ cleaning = !cleaning
+ if(!user)
+ return COMPONENT_NO_DEFAULT_MESSAGE
+ playsound(src, 'sound/items/vacuum/vacuum_clack.ogg', 30, TRUE)
+ if(cleaning) //CLEAN_SCRUB because if you get a borg to help you clean up a crime, you deserve to win.
+ balloon_alert(user, "cleaning")
+ AddComponent( \
+ /datum/component/cleaner, \
+ base_cleaning_duration = 1 SECONDS, \
+ pre_clean_callback = CALLBACK(src, PROC_REF(clean_sound)), \
+ )
+ else
+ balloon_alert(user, "vacuuming")
+ qdel(GetComponent(/datum/component/cleaner))
+ return COMPONENT_NO_DEFAULT_MESSAGE
+
+/obj/item/vacuum_item/proc/clean_sound()
+ playsound(src, 'sound/items/vacuum/vacuum_steam.ogg', 10, TRUE)
+ return CLEAN_ALLOWED
+
+/obj/item/vacuum_item/proc/retract_hose()
+ var/obj/item/borg/cleaner_box/cleaner_resolved = cleaner_box?.resolve()
+ if(!cleaner_resolved)
+ CRASH("Somehow [src] doesn't have a source to return to!")
+ if(loc == cleaner_resolved)
+ return
+ do_pickup_animation(cleaner_resolved, get_turf(src))
+ forceMove(cleaner_resolved)
+ playsound(cleaner_resolved, 'sound/items/vacuum/vacuum_ploop.ogg', 100)
+ if(!isnull(borg_hose) && !QDELING(borg_hose))
+ balloon_alert_to_hearers("snap")
+ QDEL_NULL(borg_hose)
+ bag = null
+ cleaner_resolved.deployed = FALSE
+ UnregisterSignal(src, COMSIG_ITEM_DROPPED)
+ cleaner_resolved.update_icon(UPDATE_OVERLAYS)
+
+/obj/item/vacuum_item/proc/generate_hose(mob/living/offerer, mob/living/taker)
+ var/datum/beam/held/vacuum/generated_borg_hose = new(taker, offerer, icon_state = "hosebeam", max_distance = 7, emissive = FALSE, beam_layer = BELOW_MOB_LAYER)
+ var/index = taker.get_held_index_of_item(src)
+ generated_borg_hose.lefthand = IS_LEFT_INDEX(index)
+ INVOKE_ASYNC(generated_borg_hose, TYPE_PROC_REF(/datum/beam, Start))
+ RegisterSignal(generated_borg_hose, COMSIG_QDELETING, PROC_REF(retract_hose))
+ RegisterSignal(generated_borg_hose, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(on_update))
+ return generated_borg_hose
+
+/obj/item/vacuum_item/examine(mob/user)
+ . = ..()
+ . += span_notice("Interact to switch to [cleaning ? "vacuum" : "cleaning"] mode.")
+/obj/item/vacuum_item/proc/on_update()
+ SIGNAL_HANDLER
+ var/mob/living/mob = borg_hose.origin
+ if(istype(mob))
+ var/index = mob.is_holding(src)
+ borg_hose.lefthand = IS_LEFT_INDEX(index)
+ if(prob(10))
+ playsound(src, 'sound/items/vacuum/vacuum_hose.ogg', 50, TRUE)
+
+/obj/item/vacuum_item/proc/on_drop(obj/item/vacuum, mob/living/user)
+ SIGNAL_HANDLER
+ if(user == loc)
+ return
+ retract_hose()
+
+/datum/beam/held/vacuum
+ righthand_s_px = -7
+ righthand_s_py = -3
+
+ righthand_e_px = 0
+ righthand_e_py = -6
+
+ righthand_w_px = -3
+ righthand_w_py = -6
+
+ righthand_n_px = 8
+ righthand_n_py = -6
+
+ lefthand_s_px = 7
+ lefthand_s_py = -3
+
+ lefthand_e_px = 3
+ lefthand_e_py = -6
+
+ lefthand_w_px = 0
+ lefthand_w_py = -6
+
+ lefthand_n_px = -8
+ lefthand_n_py = -6
+
+
+
diff --git a/code/modules/fishing/fishing_rod.dm b/code/modules/fishing/fishing_rod.dm
index 5d7df05303d..34c0db258ea 100644
--- a/code/modules/fishing/fishing_rod.dm
+++ b/code/modules/fishing/fishing_rod.dm
@@ -32,7 +32,7 @@
var/atom/movable/currently_hooked
/// Fishing line visual for the hooked item
- var/datum/beam/fishing_line/fishing_line
+ var/datum/beam/held/fishing_line
/// Are we currently casting
var/casting = FALSE
@@ -329,7 +329,7 @@
fishing_line.lefthand = IS_LEFT_INDEX(firer.get_held_index_of_item(src))
RegisterSignal(fishing_line, COMSIG_BEAM_BEFORE_DRAW, PROC_REF(check_los))
RegisterSignal(fishing_line, COMSIG_QDELETING, PROC_REF(clear_line))
- INVOKE_ASYNC(fishing_line, TYPE_PROC_REF(/datum/beam/, Start))
+ INVOKE_ASYNC(fishing_line, TYPE_PROC_REF(/datum/beam, Start))
if(QDELETED(fishing_line))
return null
firer.update_held_items()
@@ -919,64 +919,3 @@
QDEL_NULL(owner.fishing_line)
owner = null
return ..()
-
-/datum/beam/fishing_line
- // Is the fishing rod held in left side hand
- var/lefthand = FALSE
-
- // Make these inline with final sprites
- var/righthand_s_px = 13
- var/righthand_s_py = 16
-
- var/righthand_e_px = 18
- var/righthand_e_py = 16
-
- var/righthand_w_px = -20
- var/righthand_w_py = 18
-
- var/righthand_n_px = -14
- var/righthand_n_py = 16
-
- var/lefthand_s_px = -13
- var/lefthand_s_py = 15
-
- var/lefthand_e_px = 24
- var/lefthand_e_py = 18
-
- var/lefthand_w_px = -17
- var/lefthand_w_py = 16
-
- var/lefthand_n_px = 13
- var/lefthand_n_py = 15
-
-/datum/beam/fishing_line/Start()
- update_offsets(origin.dir)
- . = ..()
- RegisterSignal(origin, COMSIG_ATOM_DIR_CHANGE, PROC_REF(handle_dir_change))
-
-/datum/beam/fishing_line/Destroy()
- UnregisterSignal(origin, COMSIG_ATOM_DIR_CHANGE)
- . = ..()
-
-/datum/beam/fishing_line/proc/handle_dir_change(atom/movable/source, olddir, newdir)
- SIGNAL_HANDLER
- update_offsets(newdir)
- INVOKE_ASYNC(src, TYPE_PROC_REF(/datum/beam, redrawing))
-
-/datum/beam/fishing_line/proc/update_offsets(user_dir)
- switch(user_dir)
- if(SOUTH)
- override_origin_pixel_x = lefthand ? lefthand_s_px : righthand_s_px
- override_origin_pixel_y = lefthand ? lefthand_s_py : righthand_s_py
- if(EAST)
- override_origin_pixel_x = lefthand ? lefthand_e_px : righthand_e_px
- override_origin_pixel_y = lefthand ? lefthand_e_py : righthand_e_py
- if(WEST)
- override_origin_pixel_x = lefthand ? lefthand_w_px : righthand_w_px
- override_origin_pixel_y = lefthand ? lefthand_w_py : righthand_w_py
- if(NORTH)
- override_origin_pixel_x = lefthand ? lefthand_n_px : righthand_n_px
- override_origin_pixel_y = lefthand ? lefthand_n_py : righthand_n_py
-
- override_origin_pixel_x += origin.pixel_x
- override_origin_pixel_y += origin.pixel_y
diff --git a/code/modules/mob/living/living_item_handling.dm b/code/modules/mob/living/living_item_handling.dm
index 24b3e2692d1..7036f6bd65c 100644
--- a/code/modules/mob/living/living_item_handling.dm
+++ b/code/modules/mob/living/living_item_handling.dm
@@ -127,7 +127,7 @@
* Arguments:
* * offered - The player being offered the item (optional, if null the offer is to everyone around)
*/
-/mob/living/proc/give(mob/living/offered)
+/mob/living/proc/give(mob/living/offered, obj/item/item_bypass)
if(has_status_effect(/datum/status_effect/offering))
to_chat(src, span_warning("You're already offering something!"))
return
@@ -136,9 +136,9 @@
to_chat(src, span_warning("You're unable to offer anything in your current state!"))
return
- var/obj/item/offered_item = get_active_held_item()
+ var/obj/item/offered_item = item_bypass ? item_bypass : get_active_held_item()
// if it's an abstract item, should consider it to be non-existent (unless it's a HAND_ITEM, which means it's an obj/item that is just a representation of our hand)
- if(!offered_item || ((offered_item.item_flags & ABSTRACT) && !(offered_item.item_flags & HAND_ITEM)))
+ if(!offered_item || ((offered_item.item_flags & ABSTRACT && !HAS_TRAIT(offered_item, TRAIT_BORG_GIVE)) && !HAS_TRAIT(offered_item, TRAIT_OFFERED_WHEN_PULLED) && !(offered_item.item_flags & HAND_ITEM)))
to_chat(src, span_warning("You're not holding anything to offer!"))
return
@@ -187,7 +187,7 @@
* * offerer - The living mob giving the original item
* * offered_item - The item being given by the offerer
*/
-/mob/living/proc/take(mob/living/offerer, obj/item/offered_item)
+/mob/living/proc/take(mob/living/offerer, obj/item/offered_item, bypass)
clear_alert("[offerer]")
if(IS_DEAD_OR_INCAP(src))
to_chat(src, span_warning("You're unable to take anything in your current state!"))
@@ -195,7 +195,7 @@
if(get_dist(src, offerer) > 1)
to_chat(src, span_warning("[offerer] is out of range!"))
return
- if(!offered_item || offerer.get_active_held_item() != offered_item)
+ if(!offered_item || offerer.get_active_held_item() != offered_item && !bypass)
to_chat(src, span_warning("[offerer] is no longer holding the item they were offering!"))
return
if(!get_empty_held_indexes())
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index d60e9d91600..4b2a7174fb4 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -447,6 +447,7 @@
name = "Janitor"
basic_modules = list(
/obj/item/assembly/flash/cyborg,
+ /obj/item/borg/cleaner_box,
/obj/item/screwdriver/cyborg,
/obj/item/crowbar/cyborg,
/obj/item/stack/tile/iron/base/cyborg, // haha jani will have old tiles >:D
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index c9723f51e2b..12ccaf2bf20 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/mob/inhands/items/vacuum_wand_lefthand.dmi b/icons/mob/inhands/items/vacuum_wand_lefthand.dmi
new file mode 100644
index 00000000000..8bd509597bc
Binary files /dev/null and b/icons/mob/inhands/items/vacuum_wand_lefthand.dmi differ
diff --git a/icons/mob/inhands/items/vacuum_wand_righthand.dmi b/icons/mob/inhands/items/vacuum_wand_righthand.dmi
new file mode 100644
index 00000000000..5bfc27763cd
Binary files /dev/null and b/icons/mob/inhands/items/vacuum_wand_righthand.dmi differ
diff --git a/icons/obj/items_cyborg.dmi b/icons/obj/items_cyborg.dmi
index 17a65770979..4f483a4fbd8 100644
Binary files a/icons/obj/items_cyborg.dmi and b/icons/obj/items_cyborg.dmi differ
diff --git a/sound/items/vacuum/credits.txt b/sound/items/vacuum/credits.txt
new file mode 100644
index 00000000000..d068f55cdab
--- /dev/null
+++ b/sound/items/vacuum/credits.txt
@@ -0,0 +1,5 @@
+Sounds in this folder are CC-BY-NC
+
+These sounds are sourced from soundsnap under their free tier.
+
+https://soundsnap.zendesk.com/hc/en-us/articles/115003916431-Legal
diff --git a/sound/items/vacuum/vacuum_clack.ogg b/sound/items/vacuum/vacuum_clack.ogg
new file mode 100644
index 00000000000..de53eaebc91
Binary files /dev/null and b/sound/items/vacuum/vacuum_clack.ogg differ
diff --git a/sound/items/vacuum/vacuum_hose.ogg b/sound/items/vacuum/vacuum_hose.ogg
new file mode 100644
index 00000000000..fdb8b741500
Binary files /dev/null and b/sound/items/vacuum/vacuum_hose.ogg differ
diff --git a/sound/items/vacuum/vacuum_ploop.ogg b/sound/items/vacuum/vacuum_ploop.ogg
new file mode 100644
index 00000000000..39aed4c121f
Binary files /dev/null and b/sound/items/vacuum/vacuum_ploop.ogg differ
diff --git a/sound/items/vacuum/vacuum_steam.ogg b/sound/items/vacuum/vacuum_steam.ogg
new file mode 100644
index 00000000000..b56f42d95b1
Binary files /dev/null and b/sound/items/vacuum/vacuum_steam.ogg differ
diff --git a/sound/items/vacuum/vacuum_use.ogg b/sound/items/vacuum/vacuum_use.ogg
new file mode 100644
index 00000000000..17b0cb06724
Binary files /dev/null and b/sound/items/vacuum/vacuum_use.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 69551eb4a23..45c88b7d8b1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1287,6 +1287,7 @@
#include "code\datums\components\mutant_hands.dm"
#include "code\datums\components\nuclear_bomb_operator.dm"
#include "code\datums\components\object_possession.dm"
+#include "code\datums\components\offered_when_pulled.dm"
#include "code\datums\components\omen.dm"
#include "code\datums\components\onwear_mood.dm"
#include "code\datums\components\orbiter.dm"
@@ -2843,6 +2844,7 @@
#include "code\game\objects\items\robot\items\generic.dm"
#include "code\game\objects\items\robot\items\hud.dm"
#include "code\game\objects\items\robot\items\hypo.dm"
+#include "code\game\objects\items\robot\items\janitor.dm"
#include "code\game\objects\items\robot\items\storage.dm"
#include "code\game\objects\items\robot\items\tools.dm"
#include "code\game\objects\items\stacks\ammonia_crystals.dm"