mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 10:11:09 +00:00
## About The Pull Request This PR adds a skillchip that allows the user to perform normal surgeries on themselves, albeit with a 1.5x speed penalty and a flat 33% increase to the probability of screwing up each step. You can find this skillchip occasionally on the black market. It is also contraband, and can be detected by n-spect scanners. ## Why It's Good For The Game Provides a risky alternate means of obtaining surgical intervention, such as if you either can't trust anybody else to do surgery on you or can't be trusted to have certain surgeries done on you. ## Changelog 🆑 add: The Centcom Department of Import Control has become aware of the use of highly-illegal and medically dubious self-surgery skillchips aboard company stations. We suspect they are being sold to the station through the black market. /🆑
345 lines
12 KiB
Plaintext
345 lines
12 KiB
Plaintext
/// Allows an item to be used to initiate surgeries.
|
|
/datum/component/surgery_initiator
|
|
/// The currently selected target that the user is proposing a surgery on
|
|
var/datum/weakref/surgery_target_ref
|
|
|
|
/// The last user, as a weakref
|
|
var/datum/weakref/last_user_ref
|
|
|
|
/datum/component/surgery_initiator/Initialize()
|
|
. = ..()
|
|
if(!isitem(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
var/obj/item/surgery_tool = parent
|
|
surgery_tool.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS
|
|
///Make sure we can use the surgery UI while laying down
|
|
surgery_tool.interaction_flags_atom |= INTERACT_ATOM_IGNORE_MOBILITY
|
|
|
|
/datum/component/surgery_initiator/Destroy(force)
|
|
last_user_ref = null
|
|
surgery_target_ref = null
|
|
|
|
return ..()
|
|
|
|
/datum/component/surgery_initiator/RegisterWithParent()
|
|
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(initiate_surgery_moment))
|
|
RegisterSignal(parent, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context))
|
|
|
|
/datum/component/surgery_initiator/UnregisterFromParent()
|
|
UnregisterSignal(parent, COMSIG_ITEM_ATTACK, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET)
|
|
unregister_signals()
|
|
|
|
/datum/component/surgery_initiator/proc/unregister_signals()
|
|
var/mob/living/last_user = last_user_ref?.resolve()
|
|
if (!isnull(last_user_ref))
|
|
UnregisterSignal(last_user, COMSIG_MOB_SELECTED_ZONE_SET)
|
|
|
|
var/mob/living/surgery_target = surgery_target_ref?.resolve()
|
|
if (!isnull(surgery_target_ref))
|
|
UnregisterSignal(surgery_target, COMSIG_MOB_SURGERY_STARTED)
|
|
|
|
/// Does the surgery initiation.
|
|
/datum/component/surgery_initiator/proc/initiate_surgery_moment(datum/source, atom/target, mob/user)
|
|
SIGNAL_HANDLER
|
|
if(!isliving(target))
|
|
return
|
|
INVOKE_ASYNC(src, PROC_REF(do_initiate_surgery_moment), target, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
/datum/component/surgery_initiator/proc/do_initiate_surgery_moment(mob/living/target, mob/user)
|
|
var/datum/surgery/current_surgery
|
|
|
|
for(var/i_one in target.surgeries)
|
|
var/datum/surgery/surgeryloop = i_one
|
|
if(surgeryloop.location == user.zone_selected)
|
|
current_surgery = surgeryloop
|
|
break
|
|
|
|
if (!isnull(current_surgery) && !current_surgery.step_in_progress)
|
|
attempt_cancel_surgery(current_surgery, target, user)
|
|
return
|
|
|
|
var/list/available_surgeries = get_available_surgeries(user, target)
|
|
|
|
if(!length(available_surgeries))
|
|
if (target.body_position == LYING_DOWN || !(target.mobility_flags & MOBILITY_LIEDOWN))
|
|
target.balloon_alert(user, "no surgeries available!")
|
|
else
|
|
target.balloon_alert(user, "make them lie down!")
|
|
|
|
return
|
|
|
|
unregister_signals()
|
|
|
|
last_user_ref = WEAKREF(user)
|
|
surgery_target_ref = WEAKREF(target)
|
|
|
|
RegisterSignal(user, COMSIG_MOB_SELECTED_ZONE_SET, PROC_REF(on_set_selected_zone))
|
|
RegisterSignal(target, COMSIG_MOB_SURGERY_STARTED, PROC_REF(on_mob_surgery_started))
|
|
|
|
ui_interact(user)
|
|
|
|
/datum/component/surgery_initiator/proc/get_available_surgeries(mob/user, mob/living/target)
|
|
var/list/available_surgeries = list()
|
|
|
|
var/obj/item/bodypart/affecting = target.get_bodypart(check_zone(user.zone_selected))
|
|
|
|
for(var/datum/surgery/surgery as anything in GLOB.surgeries_list)
|
|
if(!surgery.possible_locs.Find(user.zone_selected))
|
|
continue
|
|
if(!is_type_in_list(target, surgery.target_mobtypes))
|
|
continue
|
|
if(user == target && !HAS_TRAIT(user, TRAIT_SELF_SURGERY) && !(surgery.surgery_flags & SURGERY_SELF_OPERABLE))
|
|
continue
|
|
|
|
if(isnull(affecting))
|
|
if(surgery.surgery_flags & SURGERY_REQUIRE_LIMB)
|
|
continue
|
|
else
|
|
if(surgery.requires_bodypart_type && !(affecting.bodytype & surgery.requires_bodypart_type))
|
|
continue
|
|
if(surgery.targetable_wound && !affecting.get_wound_type(surgery.targetable_wound))
|
|
continue
|
|
if((surgery.surgery_flags & SURGERY_REQUIRES_REAL_LIMB) && (affecting.bodypart_flags & BODYPART_PSEUDOPART))
|
|
continue
|
|
|
|
if(IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
|
|
continue
|
|
if(!surgery.can_start(user, target))
|
|
continue
|
|
|
|
available_surgeries += surgery
|
|
|
|
return available_surgeries
|
|
|
|
/// Does the surgery de-initiation.
|
|
/datum/component/surgery_initiator/proc/attempt_cancel_surgery(datum/surgery/the_surgery, mob/living/patient, mob/user)
|
|
var/selected_zone = user.zone_selected
|
|
|
|
if(the_surgery.status == 1)
|
|
patient.surgeries -= the_surgery
|
|
user.visible_message(
|
|
span_notice("[user] removes [parent] from [patient]'s [patient.parse_zone_with_bodypart(selected_zone)]."),
|
|
span_notice("You remove [parent] from [patient]'s [patient.parse_zone_with_bodypart(selected_zone)]."),
|
|
)
|
|
|
|
patient.balloon_alert(user, "stopped work on [patient.parse_zone_with_bodypart(selected_zone)]")
|
|
|
|
qdel(the_surgery)
|
|
return
|
|
|
|
var/required_tool_type = TOOL_CAUTERY
|
|
var/obj/item/close_tool = user.get_inactive_held_item()
|
|
var/is_robotic = the_surgery.requires_bodypart_type == BODYTYPE_ROBOTIC
|
|
if(is_robotic)
|
|
required_tool_type = TOOL_SCREWDRIVER
|
|
|
|
if(!close_tool || close_tool.tool_behaviour != required_tool_type)
|
|
patient.balloon_alert(user, "need a [is_robotic ? "screwdriver": "cautery"] in your inactive hand to stop the surgery!")
|
|
return
|
|
|
|
if(the_surgery.operated_bodypart)
|
|
the_surgery.operated_bodypart.adjustBleedStacks(-5)
|
|
|
|
patient.surgeries -= the_surgery
|
|
|
|
user.visible_message(
|
|
span_notice("[user] closes [patient]'s [patient.parse_zone_with_bodypart(selected_zone)] with [close_tool] and removes [parent]."),
|
|
span_notice("You close [patient]'s [patient.parse_zone_with_bodypart(selected_zone)] with [close_tool] and remove [parent]."),
|
|
)
|
|
|
|
patient.balloon_alert(user, "closed up [patient.parse_zone_with_bodypart(selected_zone)]")
|
|
|
|
qdel(the_surgery)
|
|
|
|
/datum/component/surgery_initiator/proc/on_mob_surgery_started(mob/source, datum/surgery/surgery, surgery_location)
|
|
SIGNAL_HANDLER
|
|
|
|
var/mob/living/last_user = last_user_ref.resolve()
|
|
|
|
if (surgery_location != last_user.zone_selected)
|
|
return
|
|
|
|
if (!isnull(last_user))
|
|
source.balloon_alert(last_user, "someone else started a surgery!")
|
|
|
|
ui_close()
|
|
|
|
/datum/component/surgery_initiator/proc/on_set_selected_zone(mob/source, new_zone)
|
|
ui_interact(source)
|
|
|
|
/datum/component/surgery_initiator/ui_interact(mob/user, datum/tgui/ui)
|
|
ui = SStgui.try_update_ui(user, src, ui)
|
|
if (!ui)
|
|
ui = new(user, src, "SurgeryInitiator")
|
|
ui.open()
|
|
|
|
/datum/component/surgery_initiator/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
|
. = ..()
|
|
if (.)
|
|
return .
|
|
|
|
var/mob/user = usr
|
|
var/mob/living/surgery_target = surgery_target_ref.resolve()
|
|
|
|
if (isnull(surgery_target))
|
|
return TRUE
|
|
|
|
switch (action)
|
|
if ("change_zone")
|
|
var/zone = params["new_zone"]
|
|
if (!(zone in list(
|
|
BODY_ZONE_HEAD,
|
|
BODY_ZONE_CHEST,
|
|
BODY_ZONE_L_ARM,
|
|
BODY_ZONE_R_ARM,
|
|
BODY_ZONE_L_LEG,
|
|
BODY_ZONE_R_LEG,
|
|
BODY_ZONE_PRECISE_EYES,
|
|
BODY_ZONE_PRECISE_MOUTH,
|
|
BODY_ZONE_PRECISE_GROIN,
|
|
)))
|
|
return TRUE
|
|
|
|
var/atom/movable/screen/zone_sel/zone_selector = user.hud_used?.zone_select
|
|
zone_selector?.set_selected_zone(zone, user, should_log = FALSE)
|
|
|
|
return TRUE
|
|
if ("start_surgery")
|
|
for (var/datum/surgery/surgery as anything in get_available_surgeries(user, surgery_target))
|
|
if (surgery.name == params["surgery_name"])
|
|
try_choose_surgery(user, surgery_target, surgery)
|
|
return TRUE
|
|
|
|
/datum/component/surgery_initiator/ui_assets(mob/user)
|
|
return list(
|
|
get_asset_datum(/datum/asset/simple/body_zones),
|
|
)
|
|
|
|
/datum/component/surgery_initiator/ui_data(mob/user)
|
|
var/mob/living/surgery_target = surgery_target_ref?.resolve()
|
|
|
|
var/list/surgeries = list()
|
|
if (!isnull(surgery_target))
|
|
for (var/datum/surgery/surgery as anything in get_available_surgeries(user, surgery_target))
|
|
var/list/surgery_info = list(
|
|
"name" = surgery.name,
|
|
)
|
|
|
|
if (surgery_needs_exposure(surgery, surgery_target))
|
|
surgery_info["blocked"] = TRUE
|
|
|
|
surgeries += list(surgery_info)
|
|
|
|
return list(
|
|
"selected_zone" = user.zone_selected,
|
|
"target_name" = surgery_target?.name,
|
|
"surgeries" = surgeries,
|
|
)
|
|
|
|
/datum/component/surgery_initiator/ui_close(mob/user)
|
|
unregister_signals()
|
|
surgery_target_ref = null
|
|
|
|
return ..()
|
|
|
|
/datum/component/surgery_initiator/ui_status(mob/user, datum/ui_state/state)
|
|
var/mob/living/surgery_target = surgery_target_ref?.resolve()
|
|
if (isnull(surgery_target))
|
|
return UI_CLOSE
|
|
|
|
if (!can_start_surgery(user, surgery_target))
|
|
return UI_CLOSE
|
|
|
|
return ..()
|
|
|
|
/datum/component/surgery_initiator/proc/can_start_surgery(mob/user, mob/living/target)
|
|
if (!user.Adjacent(target))
|
|
return FALSE
|
|
|
|
// The item was moved somewhere else
|
|
var/atom/movable/tool = parent
|
|
if (tool.loc != user)
|
|
return FALSE
|
|
|
|
// While we were choosing, another surgery was started at the same location
|
|
for (var/datum/surgery/surgery in target.surgeries)
|
|
if (surgery.location == user.zone_selected)
|
|
return FALSE
|
|
|
|
return TRUE
|
|
|
|
/datum/component/surgery_initiator/proc/try_choose_surgery(mob/user, mob/living/target, datum/surgery/surgery)
|
|
if (!can_start_surgery(user, target))
|
|
// This could have a more detailed message, but the UI closes when this is true anyway, so
|
|
// if it ever comes up, it'll be because of lag.
|
|
target.balloon_alert(user, "can't start the surgery!")
|
|
return
|
|
|
|
var/selected_zone = user.zone_selected
|
|
var/obj/item/bodypart/affecting_limb = target.get_bodypart(check_zone(selected_zone))
|
|
|
|
if ((surgery.surgery_flags & SURGERY_REQUIRE_LIMB) && isnull(affecting_limb))
|
|
target.balloon_alert(user, "patient has no [parse_zone(selected_zone)]!")
|
|
return
|
|
|
|
if (!isnull(affecting_limb))
|
|
if(surgery.requires_bodypart_type && !(affecting_limb.bodytype & surgery.requires_bodypart_type))
|
|
target.balloon_alert(user, "not the right type of limb!")
|
|
return
|
|
if(surgery.targetable_wound && !affecting_limb.get_wound_type(surgery.targetable_wound))
|
|
target.balloon_alert(user, "no wound to operate on!")
|
|
return
|
|
|
|
if (IS_IN_INVALID_SURGICAL_POSITION(target, surgery))
|
|
target.balloon_alert(user, "patient is not lying down!")
|
|
return
|
|
|
|
if (!surgery.can_start(user, target))
|
|
target.balloon_alert(user, "can't start the surgery!")
|
|
return
|
|
|
|
if (surgery_needs_exposure(surgery, target))
|
|
target.balloon_alert(user, "expose [target.p_their()] [target.parse_zone_with_bodypart(selected_zone)]!")
|
|
return
|
|
|
|
ui_close()
|
|
|
|
var/datum/surgery/procedure = new surgery.type(target, selected_zone, affecting_limb)
|
|
|
|
target.balloon_alert(user, "starting \"[LOWER_TEXT(procedure.name)]\"")
|
|
|
|
user.visible_message(
|
|
span_notice("[user] drapes [parent] over [target]'s [target.parse_zone_with_bodypart(selected_zone)] to prepare for surgery."),
|
|
span_notice("You drape [parent] over [target]'s [target.parse_zone_with_bodypart(selected_zone)] to prepare for \an [procedure.name]."),
|
|
)
|
|
|
|
log_combat(user, target, "operated on", null, "(OPERATION TYPE: [procedure.name]) (TARGET AREA: [selected_zone])")
|
|
|
|
/datum/component/surgery_initiator/proc/surgery_needs_exposure(datum/surgery/surgery, mob/living/target)
|
|
var/mob/living/user = last_user_ref?.resolve()
|
|
if (isnull(user))
|
|
return FALSE
|
|
if(surgery.surgery_flags & SURGERY_IGNORE_CLOTHES)
|
|
return FALSE
|
|
|
|
return !get_location_accessible(target, user.zone_selected)
|
|
|
|
/**
|
|
* Adds context sensitivy directly to the surgery initator file for screentips
|
|
* Arguments:
|
|
* * source - the surgery drapes, cloak, or bedsheet calling surgery initator
|
|
* * context - Preparing Surgery, the component has a lot of ballon alerts to deal with most contexts
|
|
* * target - the living target mob you are doing surgery on
|
|
* * user - refers to user who will see the screentip when the drapes are on a living target
|
|
*/
|
|
/datum/component/surgery_initiator/proc/add_item_context(obj/item/source, list/context, atom/target, mob/living/user,)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!isliving(target))
|
|
return NONE
|
|
|
|
context[SCREENTIP_CONTEXT_LMB] = "Prepare Surgery"
|
|
return CONTEXTUAL_SCREENTIP_SET
|