diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
index 01b8bcd576e8..d31556a57555 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm
@@ -68,10 +68,6 @@
#define COMSIG_CARBON_GAIN_ORGAN "carbon_gain_organ"
///from /item/organ/proc/Remove() (/obj/item/organ/)
#define COMSIG_CARBON_LOSE_ORGAN "carbon_lose_organ"
-///defined twice, in carbon and human's topics, fired when interacting with a valid embedded_object to pull it out (mob/living/carbon/target, /obj/item, /obj/item/bodypart/L)
-#define COMSIG_CARBON_EMBED_RIP "item_embed_start_rip"
-///called when removing a given item from a mob, from mob/living/carbon/remove_embedded_object(mob/living/carbon/target, /obj/item)
-#define COMSIG_CARBON_EMBED_REMOVAL "item_embed_remove_safe"
///Called when someone attempts to cuff a carbon
#define COMSIG_CARBON_CUFF_ATTEMPTED "carbon_attempt_cuff"
#define COMSIG_CARBON_CUFF_PREVENT (1<<0)
diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm
index 56cc96b506f0..1110c654a153 100644
--- a/code/__DEFINES/living.dm
+++ b/code/__DEFINES/living.dm
@@ -211,3 +211,6 @@
#define examining_span_normal(msg) span_infoplain(span_italics(msg))
/// For consistent examine span formatting (small size)
#define examining_span_small(msg) span_slightly_smaller(span_infoplain(span_italics(msg)))
+
+/// When a bodypart has something embedded in it
+#define COMSIG_BODYPART_ON_EMBEDDED "bodypart_on_embedded"
diff --git a/code/datums/components/embedded.dm b/code/datums/components/embedded.dm
index 9742de845bd0..809aef9ced23 100644
--- a/code/datums/components/embedded.dm
+++ b/code/datums/components/embedded.dm
@@ -81,13 +81,11 @@
/datum/component/embedded/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(jostleCheck))
- RegisterSignal(parent, COMSIG_CARBON_EMBED_RIP, PROC_REF(ripOut))
- RegisterSignal(parent, COMSIG_CARBON_EMBED_REMOVAL, PROC_REF(safeRemove))
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(checkTweeze))
RegisterSignal(parent, COMSIG_MAGIC_RECALL, PROC_REF(magic_pull))
/datum/component/embedded/UnregisterFromParent()
- UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_EMBED_RIP, COMSIG_CARBON_EMBED_REMOVAL, COMSIG_ATOM_ATTACKBY, COMSIG_MAGIC_RECALL))
+ UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_ATOM_ATTACKBY, COMSIG_MAGIC_RECALL))
/datum/component/embedded/process(seconds_per_tick)
var/mob/living/carbon/victim = parent
@@ -176,10 +174,11 @@
if(I != weapon || src.limb != limb)
return
- var/mob/living/carbon/victim = parent
- var/datum/embed_data/embed_data = weapon.get_embed()
- var/time_taken = embed_data.rip_time * weapon.w_class * 2 // melbert todo : remove this *2 when other people can rip out things from you
- INVOKE_ASYNC(src, PROC_REF(complete_rip_out), victim, I, limb, time_taken)
+ limb?.open_embed_interface(usr)
+ // var/mob/living/carbon/victim = parent
+ // var/datum/embed_data/embed_data = weapon.get_embed()
+ // var/time_taken = embed_data.rip_time * weapon.w_class * 2 // melbert todo : remove this *2 when other people can rip out things from you
+ // INVOKE_ASYNC(src, PROC_REF(complete_rip_out), victim, I, limb, time_taken)
/// everything async that ripOut used to do
/datum/component/embedded/proc/complete_rip_out(mob/living/carbon/victim, obj/item/I, obj/item/bodypart/limb, time_taken)
@@ -241,55 +240,52 @@
if(!istype(victim) || (possible_tweezers.tool_behaviour != TOOL_HEMOSTAT && possible_tweezers.tool_behaviour != TOOL_WIRECUTTER) || user.zone_selected != limb.body_zone)
return
- if(weapon != limb.embedded_objects[1]) // just pluck the first one, since we can't easily coordinate with other embedded components affecting this limb who is highest priority
- return
-
if(ishuman(victim)) // check to see if the limb is actually exposed
var/mob/living/carbon/human/victim_human = victim
if(!victim_human.try_inject(user, limb.body_zone, INJECT_CHECK_IGNORE_SPECIES | INJECT_TRY_SHOW_ERROR_MESSAGE))
- return TRUE
+ return COMPONENT_NO_AFTERATTACK
- INVOKE_ASYNC(src, PROC_REF(tweezePluck), possible_tweezers, user)
+ limb.open_embed_interface(user)
return COMPONENT_NO_AFTERATTACK
/// The actual action for pulling out an embedded object with a hemostat
-/datum/component/embedded/proc/tweezePluck(obj/item/possible_tweezers, mob/user)
- var/mob/living/carbon/victim = parent
- var/datum/embed_data/embed_data = weapon.get_embed()
- var/self_pluck = (user == victim)
- // quality of the tool we're using
- var/tweezer_speed = possible_tweezers.toolspeed
- // is this an actual piece of medical equipment
- var/tweezer_safe = (possible_tweezers.tool_behaviour == TOOL_HEMOSTAT)
- var/pluck_time = embed_data.rip_time * (weapon.w_class * 0.3) * (self_pluck ? 1.5 : 1) * tweezer_speed * (tweezer_safe ? 1 : 1.5)
-
- user.visible_message(
- span_danger("[user] begins plucking [weapon] from [user == victim ? user.p_their() : "[victim]'s"] [limb.plaintext_zone] with [possible_tweezers]..."),
- span_notice("You start plucking [weapon] from [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [possible_tweezers]... (It will take [DisplayTimeText(pluck_time)].)"),
- vision_distance = COMBAT_MESSAGE_RANGE
- )
-
- playsound(user, 'sound/surgery/hemostat1.ogg', 50, TRUE, falloff_exponent = 12, falloff_distance = 1)
- if(!do_after(user, pluck_time, victim))
- return
- if(QDELETED(src))
- return
-
- user.visible_message(
- span_danger("[user] plucks [weapon] from [victim]'s [limb.plaintext_zone][tweezer_safe ? "." : ", but hurt [victim.p_them()] in the process."]"),
- span_notice("You pluck [weapon] from [victim]'s [limb.plaintext_zone][tweezer_safe ? "." : ", but it's not perfect."]"),
- vision_distance = COMBAT_MESSAGE_RANGE,
- )
-
- var/obj/item/bodypart/our_limb = limb // because we null after removing
-
- if(!tweezer_safe)
- // sure it still hurts but it sucks less
- damaging_removal(victim, weapon, limb, (0.4 * possible_tweezers.w_class))
- safeRemove(user)
-
- if(length(our_limb.embedded_objects))
- victim.attackby(possible_tweezers, user) // loop if we can
+// /datum/component/embedded/proc/tweezePluck(obj/item/possible_tweezers, mob/user)
+// var/mob/living/carbon/victim = parent
+// var/datum/embed_data/embed_data = weapon.get_embed()
+// var/self_pluck = (user == victim)
+// // quality of the tool we're using
+// var/tweezer_speed = possible_tweezers.toolspeed
+// // is this an actual piece of medical equipment
+// var/tweezer_safe = (possible_tweezers.tool_behaviour == TOOL_HEMOSTAT)
+// var/pluck_time = embed_data.rip_time * (weapon.w_class * 0.3) * (self_pluck ? 1.5 : 1) * tweezer_speed * (tweezer_safe ? 1 : 1.5)
+
+// user.visible_message(
+// span_danger("[user] begins plucking [weapon] from [user == victim ? user.p_their() : "[victim]'s"] [limb.plaintext_zone] with [possible_tweezers]..."),
+// span_notice("You start plucking [weapon] from [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [possible_tweezers]... (It will take [DisplayTimeText(pluck_time)].)"),
+// vision_distance = COMBAT_MESSAGE_RANGE
+// )
+
+// playsound(user, 'sound/surgery/hemostat1.ogg', 50, TRUE, falloff_exponent = 12, falloff_distance = 1)
+// if(!do_after(user, pluck_time, victim))
+// return
+// if(QDELETED(src))
+// return
+
+// user.visible_message(
+// span_danger("[user] plucks [weapon] from [victim]'s [limb.plaintext_zone][tweezer_safe ? "." : ", but hurt [victim.p_them()] in the process."]"),
+// span_notice("You pluck [weapon] from [victim]'s [limb.plaintext_zone][tweezer_safe ? "." : ", but it's not perfect."]"),
+// vision_distance = COMBAT_MESSAGE_RANGE,
+// )
+
+// var/obj/item/bodypart/our_limb = limb // because we null after removing
+
+// if(!tweezer_safe)
+// // sure it still hurts but it sucks less
+// damaging_removal(victim, weapon, limb, (0.4 * possible_tweezers.w_class))
+// safeRemove(user)
+
+// if(length(our_limb.embedded_objects))
+// victim.attackby(possible_tweezers, user) // loop if we can
/// Called when an object is ripped out of someone's body by magic or other abnormal means
/datum/component/embedded/proc/magic_pull(datum/source, mob/living/caster, obj/marked_item)
diff --git a/code/datums/embed_data.dm b/code/datums/embed_data.dm
index b72d88ec15ca..847e077a7019 100644
--- a/code/datums/embed_data.dm
+++ b/code/datums/embed_data.dm
@@ -27,8 +27,6 @@ GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache())
var/impact_pain_mult = 4
/// Coefficient of multiplication for the damage the item does when it falls out or is removed without a surgery (this*item.w_class)
var/remove_pain_mult = 6
- /// Time in ticks, total removal time = (this*item.w_class)
- var/rip_time = 30
/// If this should ignore throw speed threshold of 4
var/ignore_throwspeed_threshold = FALSE
/// Chance for embedded objects to cause pain every time they move (jostle)
@@ -42,6 +40,8 @@ GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache())
var/stealthy_embed = FALSE
/// How much blood is lost per life tick while embedded
var/blood_loss = 0.25
+ /// Max speed we can pull the embedded object out without causing damage
+ var/max_pull_speed = 2
/datum/embed_data/proc/generate_with_values(
embed_chance = src.embed_chance,
@@ -50,7 +50,7 @@ GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache())
pain_mult = src.pain_mult,
impact_pain_mult = src.impact_pain_mult,
remove_pain_mult = src.remove_pain_mult,
- rip_time = src.rip_time,
+ max_pull_speed = src.max_pull_speed,
ignore_throwspeed_threshold = src.ignore_throwspeed_threshold,
jostle_chance = src.jostle_chance,
jostle_pain_mult = src.jostle_pain_mult,
@@ -66,7 +66,7 @@ GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache())
data.pain_mult = pain_mult
data.impact_pain_mult = impact_pain_mult
data.remove_pain_mult = remove_pain_mult
- data.rip_time = rip_time
+ data.max_pull_speed = max_pull_speed
data.ignore_throwspeed_threshold = ignore_throwspeed_threshold
data.jostle_chance = jostle_chance
data.jostle_pain_mult = jostle_pain_mult
@@ -86,3 +86,333 @@ GLOBAL_LIST_INIT(embed_by_type, generate_embed_type_cache())
)
if(harmful)
playsound(victim, 'sound/weapons/bladeslice.ogg', 40)
+
+
+/obj/item/bodypart
+ /// The embed interface for this limb, shared between all users viewing it
+ VAR_FINAL/atom/movable/screen/embed_interface/embed_interface
+
+/obj/item/bodypart/proc/open_embed_interface(mob/living/user = usr)
+ if(isnull(user?.client))
+ return
+
+ for(var/atom/movable/screen/embed_interface/other_embed_interface in user.client.screen)
+ if(other_embed_interface == embed_interface)
+ return // already open
+ other_embed_interface.close(user)
+
+ embed_interface ||= new(null, null, src)
+ embed_interface.open(user)
+
+/obj/effect/appearance_clone/embedded_item
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ /// Progress towards removal, or in other words y-pos
+ var/remove_progress = 0
+ /// Max speed we can pull this object out without causing damage
+ var/max_speed = INFINITY
+
+#define GET_REMOVAL_TOOL(whom, target) (whom.get_active_held_item()?.get_proxy_attacker_for(target, whom))
+
+/atom/movable/screen/embed_interface
+ icon = 'maplestation_modules/icons/hud/embed.dmi'
+ icon_state = "base"
+ screen_loc = "CENTER+2.1,CENTER-1.6"
+ maptext_x = 2
+ maptext_y = 160
+ maptext_width = 120
+ maptext_height = 200
+
+ /// Limb that owns us
+ VAR_PRIVATE/obj/item/bodypart/target_limb
+ /// Assoc list of currently tracked embedded objects to the embed holder
+ VAR_PRIVATE/list/tracked_embeds
+ /// Assoc list of mob vieweing the interface to their data
+ VAR_PRIVATE/list/viewers
+
+ /// Track the last time we attempted to pick something up while dragging
+ VAR_PRIVATE/last_drag_attempt = 0
+
+/atom/movable/screen/embed_interface/Initialize(mapload, datum/hud/hud_owner, obj/item/bodypart/limb)
+ . = ..()
+ if(isnull(limb))
+ return INITIALIZE_HINT_QDEL
+
+ maptext += ""
+ maptext += MAPTEXT_TINY_UNICODE(\
+ "Drag an object to move it. \
+ Once it enters the green area, it will be removed from the body.
\
+ Be careful, moving too fast will cause damage \
+ if you are not using tools!"\
+ )
+
+ maptext += ""
+ target_limb = limb
+ tracked_embeds = list()
+ viewers = list()
+ for (var/obj/item/embed as anything in target_limb.embedded_objects)
+ register_embedded_object(embed)
+ RegisterSignals(target_limb, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED), PROC_REF(on_limb_deleted))
+ RegisterSignal(target_limb, COMSIG_BODYPART_ON_EMBEDDED, PROC_REF(new_embed_registered))
+
+ // var/mutable_appearance/limb_underlay = new(target_limb)
+ // limb_underlay.appearance_flags |= PIXEL_SCALE
+ // limb_underlay.transform = target_limb.transform.Scale(8, 8)
+ // limb_underlay.plane = src.plane
+ // limb_underlay.layer = src.layer - 1
+ // limb_underlay.pixel_x = 45
+ // limb_underlay.pixel_y = 55
+ // limb_underlay.color = COLOR_MATRIX_GRAYSCALE
+ // underlays += limb_underlay
+
+/atom/movable/screen/embed_interface/Destroy()
+ if(target_limb)
+ UnregisterSignal(target_limb, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED, COMSIG_BODYPART_ON_EMBEDDED))
+ target_limb.embed_interface = null
+ target_limb = null
+ for(var/mob/viewer as anything in viewers)
+ close(viewer)
+ for(var/obj/item/embed as anything in tracked_embeds)
+ unregister_embedded_object(embed)
+ return ..()
+
+/atom/movable/screen/embed_interface/proc/on_limb_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ qdel(src)
+
+/atom/movable/screen/embed_interface/proc/new_embed_registered(datum/source, obj/item/embedded_item)
+ SIGNAL_HANDLER
+
+ register_embedded_object(embedded_item)
+
+/atom/movable/screen/embed_interface/proc/register_embedded_object(obj/item/embedded_item)
+
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = new(null, embedded_item)
+ embed_holder.vis_flags |= VIS_INHERIT_ID
+ embed_holder.appearance_flags |= PIXEL_SCALE
+ embed_holder.transform = embedded_item.transform.Scale(2, 2)
+ embed_holder.pixel_x = 6 + 36 * (length(tracked_embeds) % 3)
+ embed_holder.pixel_y = 2 * rand(1, 4)
+ embed_holder.layer = src.layer + 1
+ embed_holder.plane = src.plane
+
+ var/datum/embed_data/embed_data = embedded_item.get_embed()
+ embed_holder.max_speed = embed_data.max_pull_speed
+
+ tracked_embeds[embedded_item] = embed_holder
+ RegisterSignal(embedded_item, COMSIG_ITEM_UNEMBEDDED, PROC_REF(embedded_object_removed))
+ vis_contents += embed_holder
+
+/atom/movable/screen/embed_interface/proc/embedded_object_removed(obj/item/source, mob/living/owner)
+ SIGNAL_HANDLER
+ unregister_embedded_object(source)
+ if(!length(tracked_embeds))
+ qdel(src)
+ return
+
+ for(var/mob/viewer as anything in viewers)
+ if(viewers[viewer]["selected"] == source)
+ set_currently_selected(null, viewer)
+
+/atom/movable/screen/embed_interface/proc/unregister_embedded_object(obj/item/source)
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = tracked_embeds[source]
+ vis_contents -= embed_holder
+ qdel(embed_holder)
+
+ tracked_embeds -= source
+ UnregisterSignal(source, COMSIG_ITEM_UNEMBEDDED)
+
+/atom/movable/screen/embed_interface/proc/open(mob/user)
+ if(viewers[user])
+ return // already open
+ if(!isliving(user) || !user.client || !check_state(user))
+ return
+
+ RegisterSignals(user, list(COMSIG_QDELETING, COMSIG_MOB_LOGOUT), PROC_REF(on_viewer_deleted))
+ RegisterSignals(user, list(SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), COMSIG_MOVABLE_MOVED), PROC_REF(on_viewer_state_update))
+ RegisterSignals(user, list(COMSIG_MOB_DROPPING_ITEM, COMSIG_MOB_SWAP_HANDS), PROC_REF(on_viewer_hand_update))
+ user.client.screen += src
+ viewers[user] = list(
+ "selected" = null,
+ "last_move_world_time" = null,
+ "last_move_x_num" = null,
+ "last_move_y_num" = null,
+ "last_fail" = null,
+ "bypass_fail" = can_bypass_speed_check(user),
+ )
+
+/atom/movable/screen/embed_interface/proc/close(mob/user)
+ UnregisterSignal(user, list(
+ COMSIG_MOB_DROPPING_ITEM,
+ COMSIG_MOB_LOGOUT,
+ COMSIG_MOB_SWAP_HANDS,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_QDELETING,
+ SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
+ ))
+ user.client?.screen -= src
+ viewers -= user
+
+/atom/movable/screen/embed_interface/proc/check_state(mob/user)
+ if(user.incapacitated(IGNORE_STASIS|IGNORE_GRAB))
+ return FALSE
+ if(user.CanReach(target_limb.owner, GET_REMOVAL_TOOL(user, target_limb) ))
+ return TRUE
+ if(!iscarbon(user))
+ return FALSE
+ var/mob/living/carbon/carbon_mob = user
+ if(!carbon_mob.dna?.check_mutation(/datum/mutation/human/telekinesis))
+ return FALSE
+ if(!tkMaxRangeCheck(carbon_mob, target_limb.owner))
+ return FALSE
+ return TRUE
+
+/atom/movable/screen/embed_interface/proc/on_viewer_state_update(mob/source, ...)
+ SIGNAL_HANDLER
+
+ if(!check_state(source))
+ close(source)
+
+/atom/movable/screen/embed_interface/proc/on_viewer_deleted(mob/source, ...)
+ SIGNAL_HANDLER
+
+ close(source)
+
+/atom/movable/screen/embed_interface/proc/on_viewer_hand_update(mob/source, ...)
+ SIGNAL_HANDLER
+
+ viewers[source]["bypass_fail"] = can_bypass_speed_check(source)
+
+/atom/movable/screen/embed_interface/proc/set_currently_selected(obj/item/embed, mob/user)
+ if(!isnull(viewers[user]["selected"]))
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = tracked_embeds[viewers[user]["selected"]]
+ embed_holder?.remove_filter("selected") // it's fine if this one is null
+
+ viewers[user]["selected"] = embed
+ if(!isnull(embed))
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = tracked_embeds[embed]
+ if(isnull(embed_holder)) // but if this one is null we secrewed up
+ stack_trace("Embed appearance for [embed] is null in embed interface!")
+ viewers[user]["selected"] = null
+ else
+ embed_holder.add_filter("selected", 1, outline_filter(1, COLOR_YELLOW))
+
+/atom/movable/screen/embed_interface/MouseMove(location, control, params)
+ if(isnull(viewers[usr]?["selected"]))
+ return
+
+ var/list/modifiers = params2list(params)
+ var/cursor_x = text2num(LAZYACCESS(modifiers, ICON_X))
+ var/cursor_y = text2num(LAZYACCESS(modifiers, ICON_Y))
+ update_effect(cursor_x, cursor_y)
+
+/atom/movable/screen/embed_interface/MouseDrag(over_object, src_location, over_location, src_control, over_control, params)
+ var/list/modifiers = params2list(params)
+ var/cursor_x = text2num(LAZYACCESS(modifiers, ICON_X))
+ var/cursor_y = text2num(LAZYACCESS(modifiers, ICON_Y))
+ if(isnull(viewers[usr]?["selected"]))
+ if(last_drag_attempt == world.time)
+ return // drag can be sent like 100 times a tick so limit re-attempts
+ last_drag_attempt = world.time
+ var/obj/item/clicked = find_clicked_embed(cursor_x, cursor_y)
+ if(isnull(clicked))
+ return
+ set_currently_selected(clicked, usr)
+
+ update_effect(cursor_x, cursor_y)
+
+/atom/movable/screen/embed_interface/Click(location, control, params)
+ var/list/modifiers = params2list(params)
+ var/cursor_x = text2num(LAZYACCESS(modifiers, ICON_X))
+ var/cursor_y = text2num(LAZYACCESS(modifiers, ICON_Y))
+ if(cursor_x > 110 && cursor_y > 150)
+ close(usr)
+ return
+
+ var/obj/item/clicked = find_clicked_embed(cursor_x, cursor_y)
+ if(isnull(clicked))
+ return
+ if(clicked == viewers[usr]["selected"])
+ set_currently_selected(null, usr)
+ return
+
+ set_currently_selected(clicked, usr)
+ update_effect(cursor_x, cursor_y)
+
+/atom/movable/screen/embed_interface/proc/damage_limb(obj/item/from_what, multiplier = 1)
+ var/datum/embed_data/stats = from_what.get_embed()
+ target_limb.owner.sharp_pain(
+ target_zones = target_limb.body_zone,
+ amount = multiplier * clamp(from_what.w_class * stats.pain_mult * 4, 24, 48) * max(0.5, stats.pain_stam_pct),
+ dam_type = BRUTE,
+ duration = 20 SECONDS,
+ return_mod = 0.5,
+ )
+ target_limb.receive_damage(
+ brute = multiplier * clamp(from_what.w_class * stats.pain_mult * 2, 12, 24) * max(0.5, 1 - stats.pain_stam_pct),
+ wound_bonus = max(from_what.wound_bonus, 10),
+ sharpness = from_what.get_sharpness(),
+ damage_source = from_what,
+ )
+
+/atom/movable/screen/embed_interface/proc/update_effect(cursor_x, cursor_y)
+ var/last_move_world_time = viewers[usr]["last_move_world_time"]
+ var/last_move_x_num = viewers[usr]["last_move_x_num"]
+ var/last_move_y_num = viewers[usr]["last_move_y_num"]
+
+ if(!isnull(last_move_x_num) && !isnull(last_move_y_num))
+ var/obj/item/currently_selected = viewers[usr]["selected"]
+ var/dx = cursor_x - last_move_x_num
+ var/dy = cursor_y - last_move_y_num
+
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = tracked_embeds[currently_selected]
+ if(isnull(embed_holder))
+ stack_trace("Embed appearance for [currently_selected] is null in embed interface!")
+ set_currently_selected(null, usr)
+
+ // if you move too fast, it causes damage and drops the embed
+ var/time_diff = max(1, world.time - last_move_world_time)
+ var/speed = sqrt((dx * dx) + (dy * dy)) / time_diff
+ if (speed > embed_holder.max_speed && !viewers[usr]["bypass_fail"])
+ // flick("[icon_state]_fast", src)
+ icon_state = "[initial(icon_state)]_fast"
+ addtimer(VARSET_CALLBACK(src, icon_state, initial(icon_state)), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE)
+ if(embed_holder.remove_progress > 36) // damage is not applied until out of the red zone
+ embed_holder.remove_progress *= 0.9
+ damage_limb(currently_selected, (COOLDOWN_FINISHED(src, viewers[usr]["last_fail"]) ? 1 : 0.2))
+ log_combat(usr, target_limb.owner, "damaged limb by moving embedded object too quickly")
+ COOLDOWN_START(src, viewers[usr]["last_fail"], 10 SECONDS)
+ set_currently_selected(null, usr)
+
+ else
+ embed_holder.remove_progress += dy
+ if(embed_holder.remove_progress > 120)
+ var/obj/item/tool = GET_REMOVAL_TOOL(usr, target_limb)
+ if(tool?.tool_behaviour == TOOL_WIRECUTTER || tool?.get_sharpness())
+ damage_limb(currently_selected, 0.5) // you can bypass the speed limit but it still applies a bit of damage
+ log_combat(usr, target_limb.owner, "damaged limb by removing embedded object with improvised tool", tool)
+ target_limb.owner.remove_embedded_object(currently_selected, usr)
+ return
+
+ embed_holder?.pixel_x = clamp(cursor_x - 16, 8, 120)
+ embed_holder?.pixel_y = clamp(embed_holder.remove_progress, 0, 180)
+
+ viewers[usr]["last_move_world_time"] = world.time
+ viewers[usr]["last_move_x_num"] = cursor_x
+ viewers[usr]["last_move_y_num"] = cursor_y
+
+/atom/movable/screen/embed_interface/proc/can_bypass_speed_check(mob/who)
+ var/obj/item/holding = GET_REMOVAL_TOOL(who, target_limb)
+ return holding?.tool_behaviour == TOOL_HEMOSTAT || holding?.tool_behaviour == TOOL_WIRECUTTER || holding?.get_sharpness()
+
+/atom/movable/screen/embed_interface/proc/find_clicked_embed(x_num, y_num)
+ for (var/u_embed_datum, u_embed_holder in tracked_embeds)
+ var/obj/effect/appearance_clone/embedded_item/embed_holder = u_embed_holder
+ if (x_num > embed_holder.pixel_x + 52 || x_num < embed_holder.pixel_x + 12)
+ continue
+ if (y_num > embed_holder.pixel_y + 52 || y_num < embed_holder.pixel_y + 12)
+ continue
+ return u_embed_datum
+ return null
+
+#undef GET_REMOVAL_TOOL
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index e93ac7ac3ca1..bc8c90ab9abf 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1244,6 +1244,7 @@
/obj/item/proc/embedded(atom/embedded_target, obj/item/bodypart/part)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_ITEM_EMBEDDED, embedded_target, part)
+ SEND_SIGNAL(part, COMSIG_BODYPART_ON_EMBEDDED, src)
/obj/item/proc/unembedded()
if(item_flags & DROPDEL && !QDELETED(src))
diff --git a/code/game/objects/items/robot/items/food.dm b/code/game/objects/items/robot/items/food.dm
index 0920d6210113..d4c94136f178 100644
--- a/code/game/objects/items/robot/items/food.dm
+++ b/code/game/objects/items/robot/items/food.dm
@@ -238,7 +238,7 @@
ignore_throwspeed_threshold = TRUE
pain_stam_pct = 0.5
pain_mult = 3
- rip_time = 2 SECONDS
+ max_pull_speed = 10
/obj/projectile/bullet/lollipop/Initialize(mapload)
. = ..()
diff --git a/code/game/objects/items/shrapnel.dm b/code/game/objects/items/shrapnel.dm
index 01d4bfb613fc..c23626ec5523 100644
--- a/code/game/objects/items/shrapnel.dm
+++ b/code/game/objects/items/shrapnel.dm
@@ -99,7 +99,7 @@
pain_stam_pct = 0.7
pain_mult = 3
jostle_pain_mult = 3
- rip_time = 6 SECONDS
+ max_pull_speed = 1.5
/obj/projectile/bullet/pellet/stingball/on_ricochet(atom/A)
hit_prone_targets = TRUE // ducking will save you from the first wave, but not the rebounds
@@ -132,7 +132,7 @@
pain_stam_pct = 0.7
pain_mult = 5
jostle_pain_mult = 6
- rip_time = 6 SECONDS
+ max_pull_speed = 1.5
/obj/item/shrapnel/capmine
name = "\improper AP shrapnel shard"
diff --git a/code/game/objects/items/spear.dm b/code/game/objects/items/spear.dm
index 69e2f8ec5f58..bcefbe3ac537 100644
--- a/code/game/objects/items/spear.dm
+++ b/code/game/objects/items/spear.dm
@@ -40,6 +40,7 @@
impact_pain_mult = 2
remove_pain_mult = 4
jostle_chance = 2.5
+ max_pull_speed = 3
/datum/armor/item_spear
fire = 50
diff --git a/code/modules/antagonists/heretic/structures/carving_knife.dm b/code/modules/antagonists/heretic/structures/carving_knife.dm
index a1b0907ea9ba..876598ce29e0 100644
--- a/code/modules/antagonists/heretic/structures/carving_knife.dm
+++ b/code/modules/antagonists/heretic/structures/carving_knife.dm
@@ -33,7 +33,7 @@
jostle_pain_mult = 5
pain_stam_pct = 0.4
pain_mult = 3
- rip_time = 15
+ max_pull_speed = 8
/obj/item/melee/rune_carver/examine(mob/user)
. = ..()
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index 2d7c3afb71d4..4e52fb73815b 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -2,13 +2,13 @@
//as they handle all relevant stuff like adding it to the player's screen and updating their overlays.
///Returns the thing we're currently holding
-/mob/proc/get_active_held_item()
+/mob/proc/get_active_held_item() as /obj/item
return get_item_for_held_index(active_hand_index)
//Finds the opposite limb for the active one (eg: upper left arm will find the item in upper right arm)
//So we're treating each "pair" of limbs as a team, so "both" refers to them
-/mob/proc/get_inactive_held_item()
+/mob/proc/get_inactive_held_item() as /obj/item
return get_item_for_held_index(get_inactive_hand_index())
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index bbc8dbe64729..34c3699084f7 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -237,10 +237,7 @@
var/obj/item/bodypart/L = locate(href_list["embedded_limb"]) in bodyparts
if(!L)
return
- var/obj/item/I = locate(href_list["embedded_object"]) in L.embedded_objects
- if(!I || I.loc != src) //no item, no limb, or item is not in limb or in the person anymore
- return
- SEND_SIGNAL(src, COMSIG_CARBON_EMBED_RIP, I, L)
+ L.open_embed_interface(usr)
return
if(href_list["gauze_limb"])
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index 2796f58aa4de..c23d4f2bba96 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -59,7 +59,12 @@
var/list/missing = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
var/list/disabled = list()
- var/adjacent = user.Adjacent(src)
+ var/can_reach = isliving(user) && user.CanReach(src)
+ if(!can_reach && iscarbon(user))
+ var/mob/living/carbon/carbon_user = user
+ if(carbon_user.dna?.check_mutation(/datum/mutation/human/telekinesis) && tkMaxRangeCheck(user, src))
+ can_reach = TRUE
+
for(var/obj/item/bodypart/body_part as anything in bodyparts)
if(body_part.bodypart_disabled)
disabled += body_part
@@ -70,11 +75,14 @@
var/harmless = embedded.is_embed_harmless()
var/stuck_wordage = harmless ? "stuck to" : "embedded in"
var/span_to_use = harmless ? "notice" : "boldwarning"
- . += "[t_He] [t_has] [icon2html(embedded, user)] \a [embedded] [stuck_wordage] [t_his] [body_part.plaintext_zone]!"
+ var/embedded_href = "\a [embedded]"
+ if(can_reach) // only shows the href if we're adjacent
+ embedded_href = "[embedded]"
+ . += "[t_He] [t_has] [icon2html(embedded, user)] \a [embedded_href] [stuck_wordage] [t_his] [body_part.plaintext_zone]!"
if(body_part.current_gauze)
var/gauze_href = body_part.current_gauze.name
- if(adjacent && isliving(user)) // only shows the href if we're adjacent
+ if(can_reach) // only shows the href if we're adjacent
gauze_href = "[gauze_href]"
. += span_notice("There is some [icon2html(body_part.current_gauze, user)] [gauze_href] wrapped around [t_his] [body_part.plaintext_zone].")
diff --git a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
index 8595e810c3a6..d436a7a3f7a8 100644
--- a/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
+++ b/code/modules/projectiles/guns/ballistic/bows/bow_arrows.dm
@@ -43,7 +43,7 @@
pain_stam_pct = 0.5
pain_mult = 3
jostle_pain_mult = 3
- rip_time = 1 SECONDS
+ max_pull_speed = 20
/// holy arrows
/obj/item/ammo_casing/arrow/holy
diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm
index 88e1273a4fd1..7cb162bf9072 100644
--- a/code/modules/projectiles/projectile/bullets.dm
+++ b/code/modules/projectiles/projectile/bullets.dm
@@ -25,7 +25,7 @@
ignore_throwspeed_threshold = TRUE
pain_stam_pct = 0.5
pain_mult = 3
- rip_time = 10 SECONDS
+ max_pull_speed = 1.5
stealthy_embed = TRUE
blood_loss = 0.05
diff --git a/code/modules/projectiles/projectile/bullets/dart_syringe.dm b/code/modules/projectiles/projectile/bullets/dart_syringe.dm
index c0a0c4c783e6..ad5e29afa3c8 100644
--- a/code/modules/projectiles/projectile/bullets/dart_syringe.dm
+++ b/code/modules/projectiles/projectile/bullets/dart_syringe.dm
@@ -65,7 +65,7 @@
/datum/embed_data/syringe
embed_chance = 0 // only when forced
fall_chance = 0 // only when edited
- rip_time = 1.5 SECONDS
+ max_pull_speed = 5
pain_stam_pct = 0.75
impact_pain_mult = 8 // half this if syringe w class goes up.
remove_pain_mult = 8 // same
diff --git a/code/modules/projectiles/projectile/bullets/revolver.dm b/code/modules/projectiles/projectile/bullets/revolver.dm
index 182bfbddb816..e7e11e60f830 100644
--- a/code/modules/projectiles/projectile/bullets/revolver.dm
+++ b/code/modules/projectiles/projectile/bullets/revolver.dm
@@ -34,7 +34,7 @@
pain_stam_pct = 0.4
pain_mult = 3
jostle_pain_mult = 5
- rip_time = 8 SECONDS
+ max_pull_speed = 2
/obj/projectile/bullet/c38/match
name = ".38 Match bullet"
@@ -78,7 +78,7 @@
jostle_chance = 4
pain_mult = 5
jostle_pain_mult = 6
- rip_time = 5 SECONDS
+ max_pull_speed = 2.5
/obj/projectile/bullet/c38/trac
name = ".38 TRAC bullet"
diff --git a/code/modules/projectiles/projectile/bullets/rifle.dm b/code/modules/projectiles/projectile/bullets/rifle.dm
index 2a908199d9dc..92f1eecedc41 100644
--- a/code/modules/projectiles/projectile/bullets/rifle.dm
+++ b/code/modules/projectiles/projectile/bullets/rifle.dm
@@ -58,7 +58,7 @@
pain_stam_pct = 0.4
pain_mult = 5
jostle_pain_mult = 6
- rip_time = 10 SECONDS
+ max_pull_speed = 10
// Rebar (Rebar Crossbow)
/obj/projectile/bullet/rebar
@@ -83,7 +83,7 @@
pain_stam_pct = 0.4
pain_mult = 4
jostle_pain_mult = 2
- rip_time = 5 SECONDS
+ max_pull_speed = 2.75
/obj/projectile/bullet/rebarsyndie
name = "rebar"
@@ -104,4 +104,4 @@
fall_chance = 0.0006
jostle_chance = 3
pain_mult = 3
- rip_time = 6 SECONDS
+ max_pull_speed = 3
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index fd01d4da1e15..b71e41c55dc4 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -1018,6 +1018,7 @@
. += image('icons/mob/effects/dam_mob.dmi', "[dmg_overlay_type]_[body_zone]_0[burnstate]", -DAMAGE_LAYER)
var/image/limb = image(layer = -BODYPARTS_LAYER)
+ limb.appearance_flags |= PIXEL_SCALE
var/image/aux
// Handles invisibility (not alpha or actual invisibility but invisibility)
@@ -1044,6 +1045,7 @@
if(aux_zone) //Hand shit
aux = image(limb.icon, "[limb_id]_[aux_zone]", -aux_layer)
+ aux.appearance_flags |= PIXEL_SCALE
. += aux
draw_color = variable_color
if(should_draw_greyscale) //Should the limb be colored outside of a forced color?
diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm
index 3eafc75b0459..4254e612b1ab 100644
--- a/code/modules/surgery/bodyparts/helpers.dm
+++ b/code/modules/surgery/bodyparts/helpers.dm
@@ -118,14 +118,15 @@
return disabled
///Remove a specific embedded item from the carbon mob
-/mob/living/carbon/proc/remove_embedded_object(obj/item/embedded)
- SEND_SIGNAL(src, COMSIG_CARBON_EMBED_REMOVAL, embedded)
+/mob/living/carbon/proc/remove_embedded_object(obj/item/embedded, mob/remover)
+ for(var/datum/component/embedded/embedcomp as anything in GetComponents(/datum/component/embedded))
+ if(embedcomp.weapon == embedded)
+ embedcomp.safeRemove(remover) // evil embed component fuck youuu fuck youuuu
///Remove all embedded objects from all limbs on the carbon mob
-/mob/living/carbon/proc/remove_all_embedded_objects()
- for(var/obj/item/bodypart/bodypart as anything in bodyparts)
- for(var/obj/item/embedded in bodypart.embedded_objects)
- remove_embedded_object(embedded)
+/mob/living/carbon/proc/remove_all_embedded_objects(mob/remover)
+ for(var/datum/component/embedded/embedcomp as anything in GetComponents(/datum/component/embedded))
+ embedcomp.safeRemove(remover) // evil embed component fuck youuu fuck youuuu
/mob/living/carbon/proc/has_embedded_objects(include_harmless=FALSE)
for(var/obj/item/bodypart/bodypart as anything in bodyparts)
diff --git a/maplestation_modules/code/game/objects/items/other_loadout_items/loadout_inhand_items.dm b/maplestation_modules/code/game/objects/items/other_loadout_items/loadout_inhand_items.dm
index ddfbc3c38c4a..fb3fcc3e7a40 100644
--- a/maplestation_modules/code/game/objects/items/other_loadout_items/loadout_inhand_items.dm
+++ b/maplestation_modules/code/game/objects/items/other_loadout_items/loadout_inhand_items.dm
@@ -22,4 +22,4 @@
jostle_chance = 2
pain_mult = 1
jostle_pain_mult = 1.2
- rip_time = 0.5 SECONDS
+ max_pull_speed = 20
diff --git a/maplestation_modules/code/modules/projectiles/projectile/bullets/revolver.dm b/maplestation_modules/code/modules/projectiles/projectile/bullets/revolver.dm
index b279fbe4e254..207c63f9121e 100644
--- a/maplestation_modules/code/modules/projectiles/projectile/bullets/revolver.dm
+++ b/maplestation_modules/code/modules/projectiles/projectile/bullets/revolver.dm
@@ -15,7 +15,7 @@
embed_chance = 20
pain_mult = 2
jostle_pain_mult = 4
- rip_time = 4 SECONDS
+ max_pull_speed = 2.5
/obj/projectile/bullet/c38/dual_stage/fire(angle, atom/direct_target)
. = ..()
@@ -60,7 +60,7 @@
pain_stam_pct = 0.2
pain_mult = 1
jostle_pain_mult = 2
- rip_time = 1 SECONDS
+ max_pull_speed = 5
/obj/item/shrapnel/bullet/maginull
var/mob/living/carbon/spiked_mob
diff --git a/maplestation_modules/icons/hud/embed.dmi b/maplestation_modules/icons/hud/embed.dmi
new file mode 100644
index 000000000000..0dd55fd3952a
Binary files /dev/null and b/maplestation_modules/icons/hud/embed.dmi differ