/** * # Surgery Initiator * * Allows an item to start (or prematurely stop) a surgical operation. */ /datum/component/surgery_initiator /// If present, this surgery TYPE will be attempted when the item is used. /// Useful for things like limb reattachments that don't need a scalpel. var/datum/surgery/forced_surgery /// If true, the initial step will be cancellable by just using the tool again. Should be FALSE for any tool that actually has a first surgery step. var/can_cancel_before_first = FALSE /// If true, can be used with a cautery in the off-hand to cancel a surgery. var/can_cancel = TRUE /// If true, can start surgery on someone while they're standing up. /// Seeing as how we really don't support this (yet), it's much nicer to selectively enable this if we want it. var/can_start_on_stander = FALSE /// Bitfield for the types of surgeries that this can start. /// Note that in cases where organs are missing, this will be ignored. /// Also, note that for anything sharp, SURGERY_INITIATOR_ORGANIC should be set as well. var/valid_starting_types = SURGERY_INITIATOR_ORGANIC // Replace any other surgery initiator dupe_type = /datum/component/surgery_initiator /** * Create a new surgery initiating component. * * Arguments: * * forced_surgery - (optional) the surgery that will be started when the parent is used on a mob. */ /datum/component/surgery_initiator/Initialize(datum/surgery/forced_surgery) . = ..() if(!isitem(parent)) return COMPONENT_INCOMPATIBLE src.forced_surgery = forced_surgery /datum/component/surgery_initiator/RegisterWithParent() RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(initiate_surgery_moment)) RegisterSignal(parent, COMSIG_ATOM_UPDATE_SHARPNESS, PROC_REF(on_parent_sharpness_change)) /datum/component/surgery_initiator/UnregisterFromParent() UnregisterSignal(parent, COMSIG_ITEM_ATTACK) UnregisterSignal(parent, COMSIG_ATOM_UPDATE_SHARPNESS) /// Keep tabs on the attached item's sharpness. /// This component gets added in atoms when they're made sharp as well. /datum/component/surgery_initiator/proc/on_parent_sharpness_change() SIGNAL_HANDLER // COMSIG_ATOM_UPDATE_SHARPNESS var/obj/item/P = parent if(!P.sharp) RemoveComponent() qdel(src) /// Does the surgery initiation. /datum/component/surgery_initiator/proc/initiate_surgery_moment(datum/source, atom/target, mob/user) SIGNAL_HANDLER // COMSIG_ITEM_ATTACK if(!isliving(user)) return var/mob/living/L = target if(!user.Adjacent(target)) return if(user.a_intent != INTENT_HELP) return if(!IS_HORIZONTAL(L) && !can_start_on_stander) return if(IS_HORIZONTAL(L) && !on_operable_surface(L)) return if(iscarbon(target)) var/mob/living/carbon/C = target var/obj/item/organ/external/affected = C.get_organ(user.zone_selected) if(affected) if((affected.status & ORGAN_ROBOT) && !(valid_starting_types & SURGERY_INITIATOR_ROBOTIC)) return if(!(affected.status & ORGAN_ROBOT) && !(valid_starting_types & SURGERY_INITIATOR_ORGANIC)) return if(L.has_status_effect(STATUS_EFFECT_SUMMONEDGHOST)) to_chat(user, "You realise that a ghost probably doesn't have any useful organs.") return //no cult ghost surgery please INVOKE_ASYNC(src, PROC_REF(do_initiate_surgery_moment), target, user) // This signal is actually part of the attack chain, so it needs to return true to stop it return TRUE /// Meat and potatoes of starting surgery. /datum/component/surgery_initiator/proc/do_initiate_surgery_moment(mob/living/target, mob/user) var/datum/surgery/current_surgery // Check if we've already got a surgery on our target zone 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) var/datum/surgery_step/current_step = current_surgery.get_surgery_step() if(current_step.try_op(user, target, user.zone_selected, parent, current_surgery) == SURGERY_INITIATE_SUCCESS) return if(istype(parent, /obj/item/scalpel/laser/manager/debug)) return if(attempt_cancel_surgery(current_surgery, target, user)) return if(!isnull(current_surgery) && current_surgery.step_in_progress) return var/list/available_surgeries = get_available_surgeries(user, target) var/datum/surgery/procedure if(!length(available_surgeries)) if(IS_HORIZONTAL(target)) to_chat(user, "There aren't any surgeries you can perform there right now.") else to_chat(user, "You can't perform any surgeries there while [target] is standing.") return // if we have a surgery that should be performed regardless with this item, // make sure it's available to be done if(forced_surgery) for(var/datum/surgery/S in available_surgeries) if(istype(S, forced_surgery)) procedure = S break else procedure = tgui_input_list(user, "Begin which procedure?", "Surgery", available_surgeries) if(!procedure) return return try_choose_surgery(user, target, procedure) /datum/component/surgery_initiator/proc/get_available_surgeries(mob/user, mob/living/target) var/list/available_surgeries = list() for(var/datum/surgery/surgery in GLOB.surgeries_list) if(surgery.abstract && !istype(surgery, forced_surgery)) // no choosing abstract surgeries, though they can be forced continue if(!is_type_in_list(target, surgery.target_mobtypes)) continue if(!target.can_run_surgery(surgery, user)) 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 /// We haven't even started yet. Any surgery can be cancelled at this point. if(the_surgery.step_number == 1) patient.surgeries -= the_surgery user.visible_message( "[user] stops the surgery on [patient]'s [parse_zone(selected_zone)] with [parent].", "You stop the surgery on [patient]'s [parse_zone(selected_zone)] with [parent].", ) qdel(the_surgery) return TRUE if(!the_surgery.can_cancel) return // Don't make a forced surgery implement cancel a surgery. if(istype(the_surgery, forced_surgery)) return var/obj/item/close_tool var/obj/item/other_hand = user.get_inactive_hand() var/is_robotic = !the_surgery.requires_organic_bodypart var/datum/surgery_step/chosen_close_step var/skip_surgery = FALSE // if true, don't even run an operation, just end the surgery. if(!the_surgery.requires_bodypart) // special behavior here; if it doesn't require a bodypart just check if there's a limb there or not. // this is a little bit gross and I do apologize if(iscarbon(patient)) var/mob/living/carbon/C = patient var/obj/item/organ/external/affected = C.get_organ(user.zone_selected) if(!affected) skip_surgery = TRUE else // uh there's no reason this should be hit but let's be safe LOL skip_surgery = TRUE if(!skip_surgery) if(is_robotic) chosen_close_step = new /datum/surgery_step/robotics/external/close_hatch/premature() else chosen_close_step = new /datum/surgery_step/generic/cauterize/premature() if(skip_surgery) close_tool = user.get_active_hand() // sure, just something so that it isn't null else if(isrobot(user)) if(!is_robotic) // borgs need to be able to finish surgeries with just the laser scalpel, no special checks here. close_tool = parent else close_tool = locate(/obj/item/crowbar) in user.get_all_slots() if(!close_tool) to_chat(user, "You need a prying tool in an inactive slot to stop the surgery!") return TRUE else if(other_hand) for(var/key in chosen_close_step.allowed_tools) if(ispath(key) && istype(other_hand, key) || other_hand.tool_behaviour == key) close_tool = other_hand break if(!close_tool) to_chat(user, "You need a [is_robotic ? "prying": "cauterizing"] tool in your inactive hand to stop the surgery!") return TRUE if(skip_surgery || chosen_close_step.try_op(user, patient, selected_zone, close_tool, the_surgery) == SURGERY_INITIATE_SUCCESS) // logging in case people wonder why they're cut up inside log_attack(user, patient, "Prematurely finished \a [the_surgery] surgery.") qdel(chosen_close_step) patient.surgeries -= the_surgery qdel(the_surgery) // always return TRUE here so we don't continue the surgery chain and try to start a new surgery with our tool. return TRUE /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 if(!(parent in user)) to_chat(user, "You cannot start an operation if you aren't holding the tool anymore.") 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) to_chat(user, "There's already another surgery in progress on their [parse_zone(surgery.location)].") 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)) return var/obj/item/organ/affecting_limb var/selected_zone = user.zone_selected if(iscarbon(target)) var/mob/living/carbon/carbon_target = target affecting_limb = carbon_target.get_organ(check_zone(selected_zone)) if(surgery.requires_bodypart == isnull(affecting_limb)) if(surgery.requires_bodypart) to_chat(user, "The patient has no [parse_zone(selected_zone)]!") else to_chat(user, "The patient has \a [parse_zone(selected_zone)]!") return if(!isnull(affecting_limb) && (surgery.requires_organic_bodypart && affecting_limb.is_robotic()) || (!surgery.requires_organic_bodypart && !affecting_limb.is_robotic())) to_chat(user, "That's not the right type of limb for this operation!") return if(surgery.lying_required && !on_operable_surface(target)) to_chat(user, "Patient must be lying down for this operation.") return if(target == user && !surgery.self_operable) to_chat(user, "You can't perform that operation on yourself!") return if(!surgery.can_start(user, target)) to_chat(user, "Can't start the surgery!") return if(surgery_needs_exposure(surgery, target)) to_chat(user, "You have to expose [target.p_their()] [parse_zone(selected_zone)] first!") return var/datum/surgery/procedure = new surgery.type(target, selected_zone, affecting_limb) show_starting_message(user, target, procedure) log_attack(user, target, "operated on (OPERATION TYPE: [procedure.name]) (TARGET AREA: [selected_zone])") /datum/component/surgery_initiator/proc/surgery_needs_exposure(datum/surgery/surgery, mob/living/target) return !surgery.ignore_clothes && !get_location_accessible(target, target.zone_selected) /// Handle to allow for easily overriding the message shown /datum/component/surgery_initiator/proc/show_starting_message(mob/user, mob/living/target, datum/surgery/procedure) user.visible_message( "[user] holds [parent] over [target]'s [parse_zone(user.zone_selected)] to prepare for surgery.", "You hold [parent] over [target]'s [parse_zone(user.zone_selected)] to prepare for \an [procedure.name].", ) /datum/component/surgery_initiator/limb can_cancel = FALSE // don't let a leg cancel a surgery /datum/component/surgery_initiator/robo valid_starting_types = SURGERY_INITIATOR_ROBOTIC /datum/component/surgery_initiator/robo/sharp valid_starting_types = SURGERY_INITIATOR_ORGANIC | SURGERY_INITIATOR_ROBOTIC