From 4379dd2c1bc3e9f3ed0ac3342702911a0af95d72 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sun, 26 Apr 2026 15:57:59 -0400 Subject: [PATCH] Surgery Soft Requirement Refactor (#22314) Skills being based on "Hard Requirements" absolutely suck, and I wasn't happy with the surgery code being essentially hardcoded around them. So this PR refactors surgery to use soft requirement checks, based on modifiers to the success rate probability. Surgeries still state what skills (if any) they require and at what level, but rather than blocking the surgery, it instead imposes a penalty(or bonus) on the success chance based on how much the user's surgical skill differs from the required level. This actually stacks with the penalties for "non ideal tools", so if you're completely unskilled in surgery and you attempt to make an incision with a shard of glass, you're going to have an extremely hard time. Individual surgeries also can define how much the success rate is modified by "skill diff", with the more advanced "surgeon exclusive" surgeries having extremely large potential modifiers. I have also added a signal based check for modifiers, which the Morale component now hooks into. This can also allow for other components, implants, drugs, etc to modify the surgery chances in the future. I have manually tested this PR and verified by examining debug breakpoints in VSCode that the system correctly applies its modifiers. --- code/__DEFINES/dcs/signals.dm | 4 +++ .../components/morale/morale_component.dm | 9 +++++ code/datums/skills/occupational/medical.dm | 11 +++--- code/datums/skills/occupational/operations.dm | 14 +++++--- code/modules/surgery/_defines.dm | 7 ++++ code/modules/surgery/facial_surgery.dm | 7 ++++ code/modules/surgery/organs_internal.dm | 1 + code/modules/surgery/other.dm | 7 +++- code/modules/surgery/robotics.dm | 7 ++++ code/modules/surgery/surgery.dm | 36 +++++++++++++------ .../hellfirejag - surgery skill reworks.yml | 5 +++ 11 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 html/changelogs/hellfirejag - surgery skill reworks.yml 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."