diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 786b6d7b184..6573f2eec63 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -126,3 +126,7 @@
// Various computer signals for interrupting via skill or other effects.
#define COMSIG_USE_REACTOR_COMPUTER "use_reactor_computer"
#define COMSIG_USE_MECH_FAB "use_mech_fab"
+
+// Surgery Signals
+/// Signal raised against the surgeon attempting to perform a surgery to query their components for any rate mods.
+#define COMSIG_GET_SURGERY_SUCCESS_MODIFIERS "get_surgery_success_modifiers"
diff --git a/code/datums/components/morale/morale_component.dm b/code/datums/components/morale/morale_component.dm
index 5e3cbe6a142..610837e1d97 100644
--- a/code/datums/components/morale/morale_component.dm
+++ b/code/datums/components/morale/morale_component.dm
@@ -51,6 +51,9 @@
*/
var/panic_chance_ceiling = 10
+ /// The maximum possible positive or negative contribution to surgery success chances from morale modifiers.
+ var/surgery_success_contribution = 10
+
/datum/component/morale/proc/get_morale_ratio()
return morale_ratio
@@ -96,6 +99,7 @@
RegisterSignal(parent, COMSIG_MECH_MOVE_WASD, PROC_REF(handle_user_move), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_MOVE_STRAFE, PROC_REF(handle_user_strafe), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_TOGGLE_POWER, PROC_REF(handle_mech_toggle_power), override = TRUE)
+ RegisterSignal(parent, COMSIG_GET_SURGERY_SUCCESS_MODIFIERS, PROC_REF(handle_surgery_modifiers), override = TRUE)
/datum/component/morale/Destroy()
QDEL_LIST_FORCE(moodlets)
@@ -113,6 +117,7 @@
UnregisterSignal(parent, COMSIG_MECH_MOVE_WASD)
UnregisterSignal(parent, COMSIG_MECH_MOVE_STRAFE)
UnregisterSignal(parent, COMSIG_MECH_TOGGLE_POWER)
+ UnregisterSignal(parent, COMSIG_GET_SURGERY_SUCCESS_MODIFIERS)
return ..()
/datum/component/morale/process(seconds_per_tick)
@@ -228,3 +233,7 @@
*delay = *delay - (5 * morale_ratio) SECONDS
to_chat(user, SPAN_WARNING("The pressure on your mind causes you to stumble in searching for the power switch..."))
+
+/datum/component/morale/proc/handle_surgery_modifiers(mob/living/user, success_rate)
+ SIGNAL_HANDLER
+ *success_rate = *success_rate + surgery_success_contribution * morale_ratio
diff --git a/code/datums/skills/occupational/medical.dm b/code/datums/skills/occupational/medical.dm
index edf9f4a4717..c749937dd0c 100644
--- a/code/datums/skills/occupational/medical.dm
+++ b/code/datums/skills/occupational/medical.dm
@@ -11,6 +11,9 @@
/singleton/skill/surgery
name = "Surgery"
description = "Governs the user's ability to perform surgical procedures on organic humanoids, as well as what complexity of procedures can be performed. " \
+ + "A low rank in this skill causes surgery procedures to have a significantly higher chance to fail, while high ranks improve surgical chances. " \
+ + "The more advanced a surgery is, the greater the penalties will be from attempting it unskilled. " \
+ + "Having high ranks in this skill can also help offset the penalties from using non-ideal tools in surgery. " \
+ "This does not affect \"surgeries\" performed on mechanical prosthetics, robots, or synthetics in general."
maximum_level = SKILL_LEVEL_PROFESSIONAL
uneducated_skill_cap = SKILL_LEVEL_FAMILIAR // Only the most basic of all surgeries could be bought into, you'll need a real doctor education to do anything more.
@@ -20,15 +23,15 @@
required = TRUE
skill_level_descriptions = alist(
SKILL_LEVEL_UNFAMILIAR = "You have zero training or experience with surgery.
" \
- + " - You cannot perform any surgical procedures on organic humanoids.",
+ + " - You suffer a large penalty to the chances of successfully performing any surgery on organic humanoids.",
SKILL_LEVEL_FAMILIAR = "You have minimal training on the basics of surgery. This is equivalent to a fresh med school graduate, or a military corpsman.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without any penalties:
" \
+ " - Opening or Closing incisions.
" \
+ " - Clamp bleeders.
" \
+ " - Fixing Arterial Bleeding
" \
+ " - Amputating a limb.",
SKILL_LEVEL_TRAINED = "You have years of formal training and experience with surgery. This is equivalent to a fully licensed surgeon.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without any penalties:
" \
+ " - Opening or Closing incisions.
" \
+ " - Clamp bleeders.
" \
+ " - Fixing Arterial Bleeding
" \
@@ -40,7 +43,7 @@
+ " - Re-attach (organic) limbs. Robotic limbs require the Robotics skill instead.
" \
+ " - Repair non-necrotic organs other than the brain, or mechanical prosthetics.
",
SKILL_LEVEL_PROFESSIONAL = "You are a world class surgeon with decades worth of training and experience.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without any penalties:
" \
+ " - Opening or Closing incisions.
" \
+ " - Clamp bleeders.
" \
+ " - Fixing Arterial Bleeding
" \
diff --git a/code/datums/skills/occupational/operations.dm b/code/datums/skills/occupational/operations.dm
index 71728bbfc7c..120328024c7 100644
--- a/code/datums/skills/occupational/operations.dm
+++ b/code/datums/skills/occupational/operations.dm
@@ -1,6 +1,10 @@
/singleton/skill/robotics
name = "Robotics"
- description = "Governs the user's ability to perform surgical procedures on synthetics, as well as the complexity of what procedures can be performed."
+ description = "Governs the user's ability to perform surgical procedures on synthetics, as well as the complexity of what procedures can be performed. " \
+ + "A low rank in this skill causes surgery procedures to have a significantly higher chance to fail, while high ranks improve surgical chances. " \
+ + "The more advanced a surgery is, the greater the penalties will be from attempting it unskilled. " \
+ + "Having high ranks in this skill can also help offset the penalties from using non-ideal tools in surgery. " \
+ + "This does not affect \"surgeries\" performed on organics."
maximum_level = SKILL_LEVEL_PROFESSIONAL
uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
category = /singleton/skill_category/occupational
@@ -9,15 +13,15 @@
required = TRUE
skill_level_descriptions = alist(
SKILL_LEVEL_UNFAMILIAR = "You have zero training or experience with synthetics.
" \
- + " - You cannot perform any surgical procedures on synthetics.",
+ + " - You suffer a large penalty to the chances of successfully performing any surgery on synthetics.",
SKILL_LEVEL_FAMILIAR = "You have minimal training on the basics of synthetic repair and maintenance. This could be the level of a hobbyist, or someone currently pursuing a degree in robotics.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without penalties:
" \
+ " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ " - Repairing basic damage with a welder or cables.
" \
+ " - Repairing external damage to mechanical limbs.
" \
+ " - Cutting someone out of a hardsuit.",
SKILL_LEVEL_TRAINED = "You have years of formal training or experience on repairing and maintaining synthetics equivalent to a Bachelor's degree in Robotics.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without penalties:
" \
+ " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ " - Repairing basic damage with a welder or cables.
" \
+ " - Repairing external damage to mechanical limbs.
" \
@@ -30,7 +34,7 @@
+ " - Perform all forms of internal repairs to IPCs.
" \
+ " - Prepare an MMI for cyborgification.",
SKILL_LEVEL_PROFESSIONAL = "Not currently implemented, functions exactly as per Trained.
" \
- + "You can perform the following procedures:
" \
+ + "You can perform the following procedures without penalties:
" \
+ " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ " - Repairing basic damage with a welder or cables.
" \
+ " - Repairing external damage to mechanical limbs.
" \
diff --git a/code/modules/surgery/_defines.dm b/code/modules/surgery/_defines.dm
index 40f5f0b7caf..991640e29e1 100644
--- a/code/modules/surgery/_defines.dm
+++ b/code/modules/surgery/_defines.dm
@@ -24,3 +24,10 @@
//macros
#define IS_ORGAN_FULLY_OPEN affected.open == ((affected.encased || affected.robotic) ? ORGAN_ENCASED_RETRACTED : ORGAN_OPEN_RETRACTED)
+
+//skill difficulty ratings
+#define SURGERY_DIFFICULTY_TRIVIAL 10
+#define SURGERY_DIFFICULTY_EASY 15
+#define SURGERY_DIFFICULTY_MEDIUM 20
+#define SURGERY_DIFFICULTY_HARD 25
+#define SURGERY_DIFFICULTY_EXTREME 33
diff --git a/code/modules/surgery/facial_surgery.dm b/code/modules/surgery/facial_surgery.dm
index 430ea28999f..0a66b81d69d 100644
--- a/code/modules/surgery/facial_surgery.dm
+++ b/code/modules/surgery/facial_surgery.dm
@@ -27,6 +27,7 @@
min_duration = 70
max_duration = 90
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/generic/prepare_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_CUT_OPEN
@@ -60,6 +61,7 @@
min_duration = 30
max_duration = 70
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_EXTREME
/singleton/surgery_step/generic/alter_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_RETRACTED
@@ -108,6 +110,7 @@
min_duration = 50
max_duration = 80
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/face/cauterize/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face > FACE_NORMAL
@@ -152,6 +155,7 @@
min_duration = 70
max_duration = 90
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/robotics/face/synthskinopen/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face == FACE_NORMAL && target.get_species() == SPECIES_IPC_SHELL
@@ -181,6 +185,7 @@
min_duration = 70
max_duration = 90
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/robotics/face/prepare_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_CUT_OPEN
@@ -211,6 +216,7 @@
min_duration = 30
max_duration = 70
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_EXTREME
/singleton/surgery_step/robotics/face/alter_synthface/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_RETRACTED
@@ -257,6 +263,7 @@
min_duration = 50
max_duration = 80
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/robotics/face/seal_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face > FACE_NORMAL
diff --git a/code/modules/surgery/organs_internal.dm b/code/modules/surgery/organs_internal.dm
index bafe2f987a0..86a0a47a883 100644
--- a/code/modules/surgery/organs_internal.dm
+++ b/code/modules/surgery/organs_internal.dm
@@ -4,6 +4,7 @@
can_infect = TRUE
blood_level = 1
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_EXTREME
/singleton/surgery_step/internal/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/other.dm b/code/modules/surgery/other.dm
index 768c582b8a0..e7141051a38 100644
--- a/code/modules/surgery/other.dm
+++ b/code/modules/surgery/other.dm
@@ -17,6 +17,7 @@
min_duration = 40
max_duration = 60
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_HARD
/singleton/surgery_step/fix_vein/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -67,6 +68,7 @@
min_duration = 80
max_duration = 130
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_PROFESSIONAL)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_EXTREME
/singleton/surgery_step/internal/fix_dead_tissue/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -133,6 +135,7 @@
min_duration = 80
max_duration = 90
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_PROFESSIONAL)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_EXTREME
/singleton/surgery_step/treat_necrosis/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -200,6 +203,7 @@
min_duration = 50
max_duration = 70
skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/fix_tendon/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -241,7 +245,8 @@
min_duration = 100
max_duration = 160
- skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_TRIVIAL
/singleton/surgery_step/hardsuit/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/robotics.dm b/code/modules/surgery/robotics.dm
index 23a772295b5..258a519542d 100644
--- a/code/modules/surgery/robotics.dm
+++ b/code/modules/surgery/robotics.dm
@@ -473,6 +473,7 @@
min_duration = 50
max_duration = 70
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/internal/fix_internal_wiring/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -523,6 +524,7 @@
min_duration = 100
max_duration = 200
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_HARD
/singleton/surgery_step/internal/fix_internal_electronics/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -564,6 +566,7 @@
min_duration = 75
max_duration = 120
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_HARD
/singleton/surgery_step/internal/fix_internal_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -616,6 +619,7 @@
min_duration = 100
max_duration = 150
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_HARD
/singleton/surgery_step/internal/replace_internal_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -662,6 +666,7 @@
var/fast_repair = FALSE
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/internal/replace_external_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -703,6 +708,7 @@
min_duration = 100
max_duration = 150
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_TRIVIAL
/singleton/surgery_step/internal/replace_external_plating/g2
name = "Replace G2 External Armour Plating"
@@ -755,6 +761,7 @@
min_duration = 15
max_duration = 25
skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
+ skill_diff_fail_modifier = SURGERY_DIFFICULTY_MEDIUM
/singleton/surgery_step/robotics/repair_endoskeleton/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/surgery.dm b/code/modules/surgery/surgery.dm
index 83e3699e0ee..5d4d4550e6c 100644
--- a/code/modules/surgery/surgery.dm
+++ b/code/modules/surgery/surgery.dm
@@ -21,9 +21,17 @@
var/requires_surgery_compatibility = TRUE
- /// The associative list of skills and their paired requirement levels to be able to perform a given surgery.
+ /**
+ * The associative list of skills and their paired requirement levels to be able to perform a given surgery.
+ * These are considered soft requirements, so if a surgery requires skill level 3 in something, and you're at level 1
+ * Then the surgery will take a penalty of twice the skill_diff_fail_modifier (30% by default).
+ * Exceeding the skill requirement can also offset having lower success rates from things like tools.
+ */
var/alist/skill_requirements
+ /// The bonus (or penalty) fail rate to a surgery per point of skill diff. As a percent chance.
+ var/skill_diff_fail_modifier = SURGERY_DIFFICULTY_EASY
+
/// Returns how well tool is suited for this step.
/singleton/surgery_step/proc/tool_quality(obj/item/tool)
for(var/T in allowed_tools)
@@ -131,14 +139,6 @@
if(user.client) // In case of future autodocs.
S = tgui_input_list(user, "Which surgery would you like to perform?", "Surgery", possible_surgeries)
- // Check via skill components if the user knows how to perform the surgery.
- for (var/skill_comp, required_level in S.skill_requirements)
- var/skill_level = GET_SKILL_LEVEL(user, skill_comp)
- // Null condition handles NPCs and Antags that won't have the skill setup.
- if (!isnull(skill_level) && skill_level < required_level)
- to_chat(user, SPAN_WARNING("You lack the skills needed to perform this surgical procedure."))
- return TRUE
-
// We didn't find a surgery, or decided not to perform one.
if(!istype(S))
if(tool.item_flags & ITEM_FLAG_SURGERY) //Is this supposed to be used for surgery?
@@ -155,7 +155,23 @@
M.op_stage.in_progress += list(zone = user)
S.begin_step(user, M, zone, tool)
var/duration = rand(S.min_duration, S.max_duration)
- if(prob(S.tool_quality(tool)) && do_mob(user, M, duration) && !autofail)
+
+ // Get the base surgery success rate based on tools.
+ // This should eventually be reworked to use ToolQualityComponents when we add that.
+ var/success_rate = S.tool_quality(tool)
+
+ // Query the surgeon if they have any components that would like to modify the success chance.
+ SEND_SIGNAL(user, COMSIG_GET_SURGERY_SUCCESS_MODIFIERS, &success_rate)
+
+ // Skill modifier checks
+ for (var/skill_comp, required_level in S.skill_requirements)
+ var/skill_level = GET_SKILL_LEVEL(user, skill_comp)
+ // Null condition handles NPCs and Antags that won't have the skill setup.
+ if (!isnull(skill_level))
+ success_rate += (skill_level - required_level) * S.skill_diff_fail_modifier
+ // End of skill modifier checks
+
+ if(prob(success_rate) && do_mob(user, M, duration) && !autofail)
S.end_step(user, M, zone, tool)
else if ((tool in user.contents) && user.Adjacent(M))
S.fail_step(user, M, zone, tool)
diff --git a/html/changelogs/hellfirejag - surgery skill reworks.yml b/html/changelogs/hellfirejag - surgery skill reworks.yml
new file mode 100644
index 00000000000..6baa697f4a5
--- /dev/null
+++ b/html/changelogs/hellfirejag - surgery skill reworks.yml
@@ -0,0 +1,5 @@
+author: Hellfirejag
+delete-after: True
+changes:
+ - rscadd: "Reworked surgery to use soft requirements and signals to directly modify the success chances of surgery. These penalties or bonuses stack with those from not having ideal tools."
+ - rscadd: "Morale modifiers now affect surgery success rates."