From d8aa6d9696fbce4339edfccf96df85ae438a8fad Mon Sep 17 00:00:00 2001
From: Coolthulhu <Coolthulhu@gmail.com>
Date: Thu, 21 Sep 2023 15:58:59 +0200
Subject: [PATCH] Simplify broken limb mending (#3054)

* Rework broken limbs and mending

* Redesign mending_modifier: 1.0 is same as splint

* Update comments

* Update disabled effect description

---------

Co-authored-by: Olanti <olanti-p@yandex.ru>
---
 data/json/effects.json             |  14 +---
 data/json/mutations/mutations.json |  33 ++++-----
 data/json/obsoletion/effects.json  |   4 +
 src/bionics.cpp                    |  24 ------
 src/character.cpp                  |  66 +++++++++--------
 src/character.h                    |   6 +-
 src/creature.cpp                   |   4 +-
 src/iexamine.cpp                   |  15 ++--
 src/iuse_actor.cpp                 |  12 ---
 src/mutation.h                     |   6 +-
 src/mutation_data.cpp              |   2 +-
 src/npc.cpp                        |   3 +-
 src/panels.cpp                     |  45 ++++++-----
 src/player_hardcoded_effects.cpp   |   7 +-
 src/suffer.cpp                     | 115 -----------------------------
 tests/player_helpers.cpp           |   3 +
 16 files changed, 96 insertions(+), 263 deletions(-)

diff --git a/data/json/effects.json b/data/json/effects.json
index 394bfb1a915..969cafe5046 100644
--- a/data/json/effects.json
+++ b/data/json/effects.json
@@ -1802,23 +1802,11 @@
     "max_duration": "1 s",
     "rating": "bad"
   },
-  {
-    "type": "effect_type",
-    "id": "mending",
-    "name": [ "Started recovery", "Recovering", "Mostly recovered" ],
-    "desc": [ "This damaged limb is slowly regaining its functions." ],
-    "//": "Duration is 10 days, but the actual time taken is probabilistic.",
-    "max_duration": "10 d",
-    "int_dur_factor": "60 h",
-    "max_intensity": 3,
-    "rating": "good",
-    "permanent": true
-  },
   {
     "type": "effect_type",
     "id": "disabled",
     "name": [ { "ctxt": "physically", "str": "Disabled" } ],
-    "desc": [ "This limb is damaged beyond use and may require a splint to recover." ],
+    "desc": [ "This limb is damaged beyond use and must fully heal to recover.  Using a splint may speed up the process." ],
     "//": "This sounds weird. We need <bp_affected> tag or something",
     "apply_message": "Your limb breaks!",
     "remove_message": "The broken limb has mended.",
diff --git a/data/json/mutations/mutations.json b/data/json/mutations/mutations.json
index 1fe217b8559..333a471d30e 100644
--- a/data/json/mutations/mutations.json
+++ b/data/json/mutations/mutations.json
@@ -155,7 +155,7 @@
     "category": [ "MEDICAL" ],
     "healing_awake": 0.2,
     "healing_resting": 0.5,
-    "mending_modifier": 2.0
+    "mending_modifier": 0.5
   },
   {
     "type": "mutation",
@@ -722,37 +722,34 @@
     "id": "SLOWHEALER",
     "name": { "str": "Slow Healer" },
     "points": -2,
-    "description": "Your wounds heal a little slower than most.  Your HP whilst asleep, as well as your broken limbs, heal at 75% of the regular rate.",
+    "description": "Your wounds heal a little slower than most.  Your HP whilst asleep heals at 75% of the regular rate.",
     "starting_trait": true,
     "types": [ "HEALING" ],
-    "healing_resting": -0.25,
-    "mending_modifier": 0.5
+    "healing_resting": -0.25
   },
   {
     "type": "mutation",
     "id": "SLOWHEALER2",
     "name": { "str": "Poor Healer" },
     "points": -4,
-    "description": "Your health recovery is severely impaired.  Your HP whilst asleep, as well as your broken limbs, heal at 33% of the regular rate.",
+    "description": "Your health recovery is severely impaired.  Your HP whilst asleep heals at 33% of the regular rate.",
     "starting_trait": true,
     "valid": false,
     "purifiable": false,
     "types": [ "HEALING" ],
-    "healing_resting": -0.66,
-    "mending_modifier": 0.33
+    "healing_resting": -0.66
   },
   {
     "type": "mutation",
     "id": "SLOWHEALER3",
     "name": { "str": "Imperceptive Healer" },
     "points": -8,
-    "description": "Wounds are incredibly dangerous to you, as they barely heal at all.  Your HP whilst asleep, as well as your broken limbs, heal at 10% of the regular rate.",
+    "description": "Wounds are incredibly dangerous to you, as they barely heal at all.  Your HP whilst asleep heals at 10% of the regular rate.",
     "starting_trait": true,
     "valid": false,
     "purifiable": false,
     "types": [ "HEALING" ],
-    "healing_resting": -0.9,
-    "mending_modifier": 0.1
+    "healing_resting": -0.9
   },
   {
     "type": "mutation",
@@ -1377,34 +1374,34 @@
     "category": [ "PLANT", "LIZARD" ],
     "healing_awake": 0.66,
     "healing_resting": 0.5,
-    "mending_modifier": 4.0
+    "mending_modifier": 0.5
   },
   {
     "type": "mutation",
     "id": "REGEN",
     "name": { "str": "Regeneration" },
     "points": 6,
-    "description": "Your flesh regenerates from wounds incredibly quickly.  You heal 150% faster whilst asleep and 200% faster whilst awake.  Your broken limbs also heal 16 times faster than usual.",
+    "description": "Your flesh regenerates from wounds incredibly quickly.  You heal 150% faster whilst asleep and 200% faster whilst awake.  You do not require splints to heal broken limbs.",
     "types": [ "HEALING" ],
     "prereqs": [ "FASTHEALER2" ],
     "category": [ "SLIME", "TROGLOBITE" ],
     "healing_awake": 2.0,
     "healing_resting": 1.5,
-    "mending_modifier": 16.0
+    "mending_modifier": 1.0
   },
   {
     "type": "mutation",
     "id": "REGEN_LIZ",
     "name": { "str": "Reptilian Healing" },
-    "points": 5,
+    "//": "Not worth a point, barely more than flavor.",
+    "points": 0,
     "valid": false,
     "purifiable": false,
-    "description": "Your broken limbs mend themselves without significant difficulty.  You do not require splints and broken limbs heal 20 times faster than usual.",
-    "cancels": [ "ROT1", "ROT2", "ROT3" ],
-    "prereqs": [ "FASTHEALER2" ],
+    "description": "Your broken limbs mend themselves without significant difficulty.  You do not require splints.",
+    "prereqs": [ "FASTHEALER" ],
     "threshreq": [ "THRESH_LIZARD" ],
     "category": [ "LIZARD" ],
-    "mending_modifier": 20.0
+    "mending_modifier": 1.0
   },
   {
     "type": "mutation",
diff --git a/data/json/obsoletion/effects.json b/data/json/obsoletion/effects.json
index 38c218728cd..bb666566f6b 100644
--- a/data/json/obsoletion/effects.json
+++ b/data/json/obsoletion/effects.json
@@ -60,5 +60,9 @@
   {
     "type": "effect_type",
     "id": "took_anticonvulsant_visible"
+  },
+  {
+    "type": "effect_type",
+    "id": "mending"
   }
 ]
diff --git a/src/bionics.cpp b/src/bionics.cpp
index 78827e780d7..bb2b2bd73e7 100644
--- a/src/bionics.cpp
+++ b/src/bionics.cpp
@@ -101,7 +101,6 @@ static const efftype_id effect_fungus( "fungus" );
 static const efftype_id effect_hallu( "hallu" );
 static const efftype_id effect_heating_bionic( "heating_bionic" );
 static const efftype_id effect_iodine( "iodine" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_meth( "meth" );
 static const efftype_id effect_narcosis( "narcosis" );
 static const efftype_id effect_operating( "operating" );
@@ -190,7 +189,6 @@
 static const trait_id trait_PROF_AUTODOC( "PROF_AUTODOC" );
 static const trait_id trait_PROF_MED( "PROF_MED" );
 static const trait_id trait_PYROMANIA( "PYROMANIA" );
-static const trait_id trait_REGEN_LIZ( "REGEN_LIZ" );
 static const trait_id trait_THRESH_MEDICAL( "THRESH_MEDICAL" );
 
 static const flag_id flag_BIONIC_GUN( "BIONIC_GUN" );
@@ -1675,18 +1673,6 @@ void Character::process_bionic( bionic &bio )
             }
             if( calendar::once_every( 2_minutes ) ) {
                 std::vector<bodypart_id> damaged_hp_parts;
-                std::vector<effect *> mending_list;
-
-                for( const bodypart_id &bp : get_all_body_parts( true ) ) {
-                    const int hp_cur = get_part_hp_cur( bp );
-                    if( !is_limb_broken( bp ) && hp_cur < get_part_hp_max( bp ) ) {
-                        damaged_hp_parts.push_back( bp );
-                    } else if( has_effect( effect_mending, bp.id() ) &&
-                               ( has_trait( trait_REGEN_LIZ ) || worn_with_flag( flag_SPLINT, bp ) ) ) {
-                        effect *e = &get_effect( effect_mending, bp->token );
-                        mending_list.push_back( e );
-                    }
-                }
                 if( !damaged_hp_parts.empty() ) {
                     // Essential parts are considered 10 HP lower than non-essential parts for the purpose of determining priority.
                     // I'd use the essential_value, but it's tied up in the heal_actor class of iuse_actor.
@@ -1703,16 +1689,6 @@ void Character::process_bionic( bionic &bio )
                         mod_stored_kcal( -bio.info().kcal_trigger );
                     }
                 }
-                if( !mending_list.empty() ) {
-                    for( effect *e : mending_list ) {
-                        if( !can_use_bionic() ) {
-                            return;
-                        }
-                        e->mod_duration( e->get_max_duration() / 100 );
-                        mod_power_level( -bio.info().power_trigger );
-                        mod_stored_kcal( -bio.info().kcal_trigger );
-                    }
-                }
             }
         }
     } else if( bio.id == bio_painkiller ) {
diff --git a/src/character.cpp b/src/character.cpp
index 6cb10461791..761e0785f44 100644
--- a/src/character.cpp
+++ b/src/character.cpp
@@ -140,6 +140,7 @@ static const efftype_id effect_cough_suppress( "cough_suppress" );
 static const efftype_id effect_crushed( "crushed" );
 static const efftype_id effect_darkness( "darkness" );
 static const efftype_id effect_deaf( "deaf" );
+static const efftype_id effect_disabled( "disabled" );
 static const efftype_id effect_disinfected( "disinfected" );
 static const efftype_id effect_downed( "downed" );
 static const efftype_id effect_drunk( "drunk" );
@@ -167,7 +168,6 @@ static const efftype_id effect_lying_down( "lying_down" );
 static const efftype_id effect_melatonin_supplements( "melatonin" );
 static const efftype_id effect_meth( "meth" );
 static const efftype_id effect_masked_scent( "masked_scent" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_narcosis( "narcosis" );
 static const efftype_id effect_nausea( "nausea" );
 static const efftype_id effect_no_sight( "no_sight" );
@@ -287,7 +287,6 @@ static const trait_id trait_INFRARED( "INFRARED" );
 static const trait_id trait_LEG_TENT_BRACE( "LEG_TENT_BRACE" );
 static const trait_id trait_LIGHT_BONES( "LIGHT_BONES" );
 static const trait_id trait_LIZ_IR( "LIZ_IR" );
-static const trait_id trait_REGEN_LIZ( "REGEN_LIZ" );
 static const trait_id trait_M_DEPENDENT( "M_DEPENDENT" );
 static const trait_id trait_M_IMMUNE( "M_IMMUNE" );
 static const trait_id trait_M_SKIN2( "M_SKIN2" );
@@ -1257,14 +1256,15 @@ int Character::get_working_leg_count() const
 
 bool Character::is_limb_disabled( const bodypart_id &limb ) const
 {
-    return get_part_hp_cur( limb ) <= get_part_hp_max( limb ) * .125;
+    return is_limb_broken( limb ) ||
+           ( get_part_hp_cur( limb ) <= get_part_hp_max( limb ) * 0.125 );
 }
 
 // this is the source of truth on if a limb is broken so all code to determine
 // if a limb is broken should point here to make any future changes to breaking easier
 bool Character::is_limb_broken( const bodypart_id &limb ) const
 {
-    return get_part_hp_cur( limb ) <= 0;
+    return has_effect( effect_disabled, limb.id() );
 }
 
 bool Character::can_run()
@@ -4580,8 +4580,17 @@ void Character::regen( int rate_multiplier )
 
     float rest = rest_quality();
     float heal_rate = healing_rate( rest ) * to_turns<int>( 5_minutes );
+    const float broken_regen_mod = clamp( mutation_value( "mending_modifier" ), 0.25f, 1.0f );
     if( heal_rate > 0.0f ) {
-        healall( roll_remainder( rate_multiplier * heal_rate ) );
+        const int base_heal = roll_remainder( rate_multiplier * heal_rate );
+        const int broken_heal = roll_remainder( base_heal * broken_regen_mod );
+
+        for( const bodypart_id &bp : get_all_body_parts() ) {
+            const bool is_broken = is_limb_broken( bp ) &&
+                                   !worn_with_flag( flag_SPLINT, bp );
+            heal( bp, is_broken ? broken_heal : base_heal );
+            mod_part_healed_total( bp, is_broken ? broken_heal : base_heal );
+        }
     } else if( heal_rate < 0.0f ) {
         int rot_rate = roll_remainder( rate_multiplier * -heal_rate );
         // Has to be in loop because some effects depend on rounding
@@ -4595,9 +4604,13 @@ void Character::regen( int rate_multiplier )
         const bodypart_id &bp = convert_bp( hp_to_bp( static_cast<hp_part>( i ) ) ).id();
         float healing = healing_rate_medicine( rest, bp ) * to_turns<int>( 5_minutes );
 
-        int healing_apply = roll_remainder( healing );
+        const bool is_broken = is_limb_broken( bp ) &&
+                               !worn_with_flag( flag_SPLINT, bp );
+        const int healing_apply = roll_remainder( is_broken ? healing *broken_regen_mod : healing );
+
         healed_bp( i, healing_apply );
         heal( bp, healing_apply );
+
         if( damage_bandaged[i] > 0 ) {
             damage_bandaged[i] -= healing_apply;
             if( damage_bandaged[i] <= 0 ) {
@@ -4707,9 +4720,6 @@ void Character::update_body( const time_point &from, const time_point &to )
         check_needs_extremes();
         update_needs( five_mins );
         regen( five_mins );
-        // Note: mend ticks once per 5 minutes, but wants rate in TURNS, not 5 minute intervals
-        // TODO: change @ref med to take time_duration
-        mend( five_mins * to_turns<int>( 5_minutes ) );
     }
     if( ticks_between( from, to, 24_hours ) > 0 ) {
         enforce_minimum_healing();
@@ -5800,15 +5810,11 @@ hp_part Character::body_window( const std::string &menu_header,
         const nc_color all_state_col = limb_color( bp, true, true, true );
         // Broken means no HP can be restored, it requires surgical attention.
         const bool limb_is_broken = is_limb_broken( bp );
-        const bool limb_is_mending = limb_is_broken &&
-                                     ( worn_with_flag( flag_SPLINT, bp ) || has_trait( trait_REGEN_LIZ ) );
 
         if( show_all ) {
             e.allowed = true;
         } else if( has_curable_effect ) {
             e.allowed = true;
-        } else if( limb_is_broken ) {
-            e.allowed = false;
         } else if( current_hp < maximal_hp && ( e.bonus != 0 || bandage_power > 0.0f  ||
                                                 disinfectant_power > 0.0f ) ) {
             e.allowed = true;
@@ -5837,21 +5843,21 @@ hp_part Character::body_window( const std::string &menu_header,
 
         const auto &aligned_name = std::string( max_bp_name_len - utf8_width( e.name ), ' ' ) + e.name;
         std::string hp_str;
-        if( limb_is_mending ) {
-            desc += colorize( _( "It is broken but has been set and just needs time to heal." ),
+        if( limb_is_broken ) {
+            const nc_color color = worn_with_flag( flag_SPLINT, bp ) ||
+                                   ( mutation_value( "mending_modifier" ) >= 1.0f ) ?
+                                   c_blue :
+                                   c_light_red;
+            desc += colorize( _( "It is broken and must heal fully before it becomes functional again." ),
                               c_blue ) + "\n";
-            const auto &eff = get_effect( effect_mending, bp_token );
-            const int mend_perc = eff.is_null() ? 0.0 : 100 * eff.get_duration() / eff.get_max_duration();
+            const int mend_perc = 100 * current_hp / maximal_hp;
 
             if( precise ) {
-                hp_str = colorize( string_format( "=%2d%%=", mend_perc ), c_blue );
+                hp_str = colorize( string_format( "=%2d%%=", mend_perc ), color );
             } else {
                 const int num = mend_perc / 20;
-                hp_str = colorize( std::string( num, '#' ) + std::string( 5 - num, '=' ), c_blue );
+                hp_str = colorize( std::string( num, '#' ) + std::string( 5 - num, '=' ), color );
             }
-        } else if( limb_is_broken ) {
-            desc += colorize( _( "It is broken.  It needs a splint or surgical attention." ), c_red ) + "\n";
-            hp_str = "==%==";
         } else if( precise ) {
             hp_str = string_format( "%d", current_hp );
         } else {
@@ -8431,11 +8437,6 @@ void Character::apply_damage( Creature *source, bodypart_id hurt, int dam,
         put_into_vehicle_or_drop( *this, item_drop_reason::tumbling, { weapon } );
         i_rem( &weapon );
     }
-    if( has_effect( effect_mending, part_to_damage->token ) ) {
-        effect &e = get_effect( effect_mending, part_to_damage->token );
-        float remove_mend = dam / 20.0f;
-        e.mod_duration( -e.get_max_duration() * remove_mend );
-    }
 
     if( dam > get_painkiller() ) {
         on_hurt( source );
@@ -8646,10 +8647,13 @@ int Character::reduce_healing_effect( const efftype_id &eff_id, int remove_med,
 
 void Character::heal( const bodypart_id &healed, int dam )
 {
-    if( !is_limb_broken( healed ) ) {
-        int effective_heal = std::min( dam, get_part_hp_max( healed ) - get_part_hp_cur( healed ) );
-        mod_part_hp_cur( healed, effective_heal );
-        g->events().send<event_type::character_heals_damage>( getID(), effective_heal );
+    const int max_hp = get_part_hp_max( healed );
+    const int cur_hp = get_part_hp_cur( healed );
+    const int effective_heal = std::min( dam, max_hp - cur_hp );
+    mod_part_hp_cur( healed, effective_heal );
+    g->events().send<event_type::character_heals_damage>( getID(), effective_heal );
+    if( cur_hp + dam >= max_hp ) {
+        remove_effect( effect_disabled, healed.id() );
     }
 }
 
diff --git a/src/character.h b/src/character.h
index c3ff9d3f7eb..a84bf824865 100644
--- a/src/character.h
+++ b/src/character.h
@@ -742,10 +742,8 @@ class Character : public Creature, public visitable<Character>
         int get_working_arm_count() const;
         /** Returns the number of functioning legs */
         int get_working_leg_count() const;
-        /** Returns true if the limb is disabled(12.5% or less hp)*/
+        /** Returns true if the limb is disabled (12.5% or less hp, or broken)*/
         bool is_limb_disabled( const bodypart_id &limb ) const;
-        /** Returns true if the limb is hindered(40% or less hp) */
-        bool is_limb_hindered( hp_part limb ) const;
         /** Returns true if the limb is broken */
         bool is_limb_broken( const bodypart_id &limb ) const;
         /** source of truth of whether a Character can run */
@@ -2063,8 +2061,6 @@ class Character : public Creature, public visitable<Character>
         void suffer();
         /** Handles mitigation and application of radiation */
         bool irradiate( float rads, bool bypass = false );
-        /** Handles the chance for broken limbs to spontaneously heal to 1 HP */
-        void mend( int rate_multiplier );
 
         /** Creates an auditory hallucination */
         void sound_hallu();
diff --git a/src/creature.cpp b/src/creature.cpp
index 0d69db54329..d1c48950682 100644
--- a/src/creature.cpp
+++ b/src/creature.cpp
@@ -1652,14 +1652,14 @@ void Creature::mod_part_healed_total( const bodypart_id &id, int mod )
 void Creature::set_all_parts_hp_cur( const int set )
 {
     for( std::pair<const bodypart_str_id, bodypart> &elem : body ) {
-        elem.second.set_hp_cur( set );
+        set_part_hp_cur( elem.first, set );
     }
 }
 
 void Creature::set_all_parts_hp_to_max()
 {
     for( std::pair<const bodypart_str_id, bodypart> &elem : body ) {
-        elem.second.set_hp_to_max();
+        set_part_hp_cur( elem.first, get_part_hp_max( elem.first ) );
     }
 }
 
diff --git a/src/iexamine.cpp b/src/iexamine.cpp
index cb1af8fb5a7..5295e4bf0ca 100644
--- a/src/iexamine.cpp
+++ b/src/iexamine.cpp
@@ -123,7 +123,6 @@ static const efftype_id effect_bleed( "bleed" );
 static const efftype_id effect_disinfected( "disinfected" );
 static const efftype_id effect_earphones( "earphones" );
 static const efftype_id effect_infected( "infected" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_pblue( "pblue" );
 static const efftype_id effect_pkill2( "pkill2" );
 static const efftype_id effect_sleep( "sleep" );
@@ -5081,11 +5080,7 @@ void iexamine::autodoc( player &p, const tripoint &examp )
             for( int i = 0; i < num_hp_parts; i++ ) {
                 const bodypart_id &part = convert_bp( player::hp_to_bp( static_cast<hp_part>( i ) ) ).id();
                 const bool broken = patient.is_limb_broken( part );
-                effect &existing_effect = patient.get_effect( effect_mending, part->token );
-                // Skip part if not broken or already healed 50%
-                if( !broken || ( !existing_effect.is_null() &&
-                                 existing_effect.get_duration() >
-                                 existing_effect.get_max_duration() - 5_days - 1_turns ) ) {
+                if( !broken ) {
                     continue;
                 }
                 broken_limbs_count++;
@@ -5116,9 +5111,11 @@ void iexamine::autodoc( player &p, const tripoint &examp )
                     patient.add_msg_player_or_npc( m_good, _( "The machine rapidly sets and splints your broken %s." ),
                                                    _( "The machine rapidly sets and splints <npcname>'s broken %s." ),
                                                    body_part_name( part ) );
-                    patient.add_effect( effect_mending, 0_turns, part->token );
-                    effect &mending_effect = patient.get_effect( effect_mending, part->token );
-                    mending_effect.set_duration( mending_effect.get_max_duration() - 5_days );
+                    // TODO: Prevent exploits with hp draining stuff?
+                    int heal_amt = patient.get_part_hp_max( part ) / 2 - patient.get_part_hp_cur( part );
+                    if( heal_amt > 0 ) {
+                        patient.heal( part, heal_amt );
+                    }
                 }
             }
             if( broken_limbs_count == 0 ) {
diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp
index a0d6e66b7dc..4a0eb9befdb 100644
--- a/src/iuse_actor.cpp
+++ b/src/iuse_actor.cpp
@@ -3903,18 +3903,6 @@ static hp_part pick_part_to_heal(
             return healed_part;
         }
 
-        if( patient.is_limb_broken( bp ) ) {
-            if( healed_part == hp_arm_l || healed_part == hp_arm_r ) {
-                add_msg( m_info, _( "That arm is broken.  It needs surgical attention or a splint." ) );
-            } else if( healed_part == hp_leg_l || healed_part == hp_leg_r ) {
-                add_msg( m_info, _( "That leg is broken.  It needs surgical attention or a splint." ) );
-            } else {
-                add_msg( m_info, "That body part is bugged.  It needs developer's attention." );
-            }
-
-            continue;
-        }
-
         if( force || patient.get_part_hp_cur( bp ) < patient.get_part_hp_max( bp ) ) {
             return healed_part;
         }
diff --git a/src/mutation.h b/src/mutation.h
index 8e82983e2d7..8fe226d47b5 100644
--- a/src/mutation.h
+++ b/src/mutation.h
@@ -115,8 +115,10 @@ struct mutation_branch {
         // Healing per turn
         float healing_awake = 0.0f;
         float healing_resting = 0.0f;
-        // Limb mending bonus
-        float mending_modifier = 1.0f;
+        // Multiplier on regen of broken limbs.
+        // Base regen of broken limbs is 25% and 25% the low cap.
+        // Capped at 1.0, which makes broken limbs regen at same rate as unbroken.
+        float mending_modifier = 0.0f;
         // Bonus HP multiplier. That is, 1.0 doubles hp, -0.5 halves it.
         float hp_modifier = 0.0f;
         // Second HP modifier that stacks with first but is otherwise identical.
diff --git a/src/mutation_data.cpp b/src/mutation_data.cpp
index 9c95b97873e..a070de723d8 100644
--- a/src/mutation_data.cpp
+++ b/src/mutation_data.cpp
@@ -377,7 +377,7 @@ void mutation_branch::load( const JsonObject &jo, const std::string & )
     optional( jo, was_loaded, "pain_recovery", pain_recovery, 0.0f );
     optional( jo, was_loaded, "healing_awake", healing_awake, 0.0f );
     optional( jo, was_loaded, "healing_resting", healing_resting, 0.0f );
-    optional( jo, was_loaded, "mending_modifier", mending_modifier, 1.0f );
+    optional( jo, was_loaded, "mending_modifier", mending_modifier, 0.0f );
     optional( jo, was_loaded, "hp_modifier", hp_modifier, 0.0f );
     optional( jo, was_loaded, "hp_modifier_secondary", hp_modifier_secondary, 0.0f );
     optional( jo, was_loaded, "hp_adjustment", hp_adjustment, 0.0f );
diff --git a/src/npc.cpp b/src/npc.cpp
index 36b63dd2bb4..9c31869540d 100644
--- a/src/npc.cpp
+++ b/src/npc.cpp
@@ -84,7 +84,6 @@ static const efftype_id effect_contacts( "contacts" );
 static const efftype_id effect_drunk( "drunk" );
 static const efftype_id effect_feral_killed_recently( "feral_killed_recently" );
 static const efftype_id effect_infection( "infection" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_npc_flee_player( "npc_flee_player" );
 static const efftype_id effect_npc_suspend( "npc_suspend" );
 static const efftype_id effect_pkill_l( "pkill_l" );
@@ -1073,7 +1072,8 @@ bool npc::wear_if_wanted( const item &it, std::string &reason )
         for( int i = 0; i < num_hp_parts; i++ ) {
             hp_part hpp = static_cast<hp_part>( i );
             body_part bp = player::hp_to_bp( hpp );
-            if( is_limb_broken( convert_bp( bp ) ) && !has_effect( effect_mending, bp ) &&
+            if( is_limb_broken( convert_bp( bp ).id() ) &&
+                !worn_with_flag( flag_SPLINT, convert_bp( bp ).id() ) &&
                 it.covers( convert_bp( bp ).id() ) ) {
                 reason = _( "Thanks, I'll wear that now." );
                 return !!wear_item( it, false );
diff --git a/src/panels.cpp b/src/panels.cpp
index 41983b82ae5..9c8ae1701bc 100644
--- a/src/panels.cpp
+++ b/src/panels.cpp
@@ -61,7 +61,6 @@
 #include "vpart_position.h"
 #include "weather.h"
 
-static const trait_id trait_REGEN_LIZ( "REGEN_LIZ" );
 static const trait_id trait_SELFAWARE( "SELFAWARE" );
 static const trait_id trait_THRESH_FELINE( "THRESH_FELINE" );
 static const trait_id trait_THRESH_BIRD( "THRESH_BIRD" );
@@ -771,36 +770,36 @@ static void draw_limb_health( avatar &u, const catacurses::window &w, int limb_i
             wprintz( w, color, sym );
         }
     };
+
     const bodypart_id bp = convert_bp( avatar::hp_to_bp( static_cast<hp_part>( limb_index ) ) ).id();
+    const int hp_cur = u.get_part_hp_cur( bp );
+    const int hp_max = u.get_part_hp_max( bp );
+
+    std::optional<nc_color> color_override;
+
     if( u.is_limb_broken( bp.id() ) && ( limb_index >= hp_arm_l &&
                                          limb_index <= hp_leg_r ) ) {
         //Limb is broken
-        std::string limb = "~~%~~";
-        nc_color color = c_light_red;
-
-        if( u.worn_with_flag( json_flag_SPLINT,  bp ) || u.has_trait( trait_REGEN_LIZ ) ) {
-            static const efftype_id effect_mending( "mending" );
-            const auto &eff = u.get_effect( effect_mending, bp->token );
-            const int mend_perc = eff.is_null() ? 0.0 : 100 * eff.get_duration() / eff.get_max_duration();
+        const int mend_perc =  100 * hp_cur / hp_max;
+        bool splinted = u.worn_with_flag( json_flag_SPLINT, bp ) ||
+                        ( u.mutation_value( "mending_modifier" ) >= 1.0f );
+        nc_color color = splinted ? c_blue : c_dark_gray;
 
-            if( is_self_aware || u.has_effect( effect_got_checked ) ) {
-                limb = string_format( "=%2d%%=", mend_perc );
-                color = c_blue;
-            } else {
-                const int num = mend_perc / 20;
-                print_symbol_num( w, num, "#", c_blue );
-                print_symbol_num( w, 5 - num, "=", c_blue );
-                return;
-            }
+        if( is_self_aware || u.has_effect( effect_got_checked ) ) {
+            color_override = color;
+        } else {
+            const int num = mend_perc / 20;
+            print_symbol_num( w, num, "#", color );
+            print_symbol_num( w, 5 - num, "=", color );
+            return;
         }
-
-        wprintz( w, color, limb );
-        return;
     }
 
-    const int hp_cur = u.get_part_hp_cur( bp );
-    const int hp_max = u.get_part_hp_max( bp );
+
     std::pair<std::string, nc_color> hp = get_hp_bar( hp_cur, hp_max );
+    if( color_override ) {
+        hp.second = *color_override;
+    }
 
     if( is_self_aware || u.has_effect( effect_got_checked ) ) {
         wprintz( w, hp.second, "%3d  ", hp_cur );
diff --git a/src/player_hardcoded_effects.cpp b/src/player_hardcoded_effects.cpp
index 2f74c64ccf4..88d7f83de8e 100644
--- a/src/player_hardcoded_effects.cpp
+++ b/src/player_hardcoded_effects.cpp
@@ -73,7 +73,6 @@ static const efftype_id effect_hallu( "hallu" );
 static const efftype_id effect_hot( "hot" );
 static const efftype_id effect_infected( "infected" );
 static const efftype_id effect_lying_down( "lying_down" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_mutating( "mutating" );
 static const efftype_id effect_nausea( "nausea" );
 static const efftype_id effect_narcosis( "narcosis" );
@@ -1292,12 +1291,8 @@ void Character::hardcoded_effects( effect &it )
                 }
             }
         }
-    } else if( id == effect_mending ) {
-        if( !is_limb_broken( convert_bp( bp ) ) ) {
-            it.set_duration( 0_turns );
-        }
     } else if( id == effect_disabled ) {
-        if( !is_limb_broken( convert_bp( bp ) ) ) {
+        if( get_part_hp_cur( convert_bp( bp ) ) >= get_part_hp_max( convert_bp( bp ) ) ) {
             remove_effect( effect_disabled );
         }
     } else if( id == effect_panacea ) {
diff --git a/src/suffer.cpp b/src/suffer.cpp
index f752944f22b..acc6a8dc50e 100644
--- a/src/suffer.cpp
+++ b/src/suffer.cpp
@@ -95,7 +95,6 @@ static const efftype_id effect_glowy_led( "glowy_led" );
 static const efftype_id effect_hallu( "hallu" );
 static const efftype_id effect_iodine( "iodine" );
 static const efftype_id effect_masked_scent( "masked_scent" );
-static const efftype_id effect_mending( "mending" );
 static const efftype_id effect_meth( "meth" );
 static const efftype_id effect_narcosis( "narcosis" );
 static const efftype_id effect_nausea( "nausea" );
@@ -143,7 +142,6 @@ static const trait_id trait_RADIOACTIVE1( "RADIOACTIVE1" );
 static const trait_id trait_RADIOACTIVE2( "RADIOACTIVE2" );
 static const trait_id trait_RADIOACTIVE3( "RADIOACTIVE3" );
 static const trait_id trait_RADIOGENIC( "RADIOGENIC" );
-static const trait_id trait_REGEN_LIZ( "REGEN_LIZ" );
 static const trait_id trait_ROOTS3( "ROOTS3" );
 static const trait_id trait_SCHIZOPHRENIC( "SCHIZOPHRENIC" );
 static const trait_id trait_SHARKTEETH( "SHARKTEETH" );
@@ -174,16 +172,6 @@ static const std::string flag_RAD_RESIST( "RAD_RESIST" );
 static const std::string flag_SPLINT( "SPLINT" );
 static const std::string flag_SUN_GLASSES( "SUN_GLASSES" );
 
-static float addiction_scaling( float at_min, float at_max, float add_lvl )
-{
-    // Not addicted
-    if( add_lvl < MIN_ADDICTION_LEVEL ) {
-        return 1.0f;
-    }
-
-    return lerp( at_min, at_max, ( add_lvl - MIN_ADDICTION_LEVEL ) / MAX_ADDICTION_LEVEL );
-}
-
 void Character::suffer_water_damage( const mutation_branch &mdata )
 {
     for( const std::pair<const bodypart_str_id, bodypart> &elem : get_body() ) {
@@ -1669,109 +1657,6 @@ bool Character::irradiate( float rads, bool bypass )
     return false;
 }
 
-void Character::mend( int rate_multiplier )
-{
-    // Wearing splints can slowly mend a broken limb back to 1 hp.
-    bool any_broken = false;
-    for( const bodypart_id &bp : get_all_body_parts() ) {
-        if( is_limb_broken( bp ) ) {
-            any_broken = true;
-            break;
-        }
-    }
-
-    if( !any_broken ) {
-        return;
-    }
-
-    double healing_factor = 1.0;
-    // Studies have shown that alcohol and tobacco use delay fracture healing time
-    // Being under effect is 50% slowdown
-    // Being addicted but not under effect scales from 25% slowdown to 75% slowdown
-    // The improvement from being intoxicated over withdrawal is intended
-    if( has_effect( effect_cig ) ) {
-        healing_factor *= 0.5;
-    } else {
-        healing_factor *= addiction_scaling( 0.25f, 0.75f, addiction_level( add_type::CIG ) );
-    }
-
-    if( has_effect( effect_drunk ) ) {
-        healing_factor *= 0.5;
-    } else {
-        healing_factor *= addiction_scaling( 0.25f, 0.75f, addiction_level( add_type::ALCOHOL ) );
-    }
-
-    if( get_rad() > 0 && !has_trait( trait_RADIOGENIC ) ) {
-        healing_factor *= clamp( ( 1000.0f - get_rad() ) / 1000.0f, 0.0f, 1.0f );
-    }
-
-    // Bed rest speeds up mending
-    if( has_effect( effect_sleep ) ) {
-        healing_factor *= 4.0;
-    } else if( get_fatigue() > fatigue_levels::dead_tired ) {
-        // but being dead tired does not...
-        healing_factor *= 0.75;
-    } else {
-        // If not dead tired, resting without sleep also helps
-        healing_factor *= 1.0f + rest_quality();
-    }
-
-    // Being healthy helps.
-    healing_factor *= 1.0f + get_healthy() / 200.0f;
-
-    // Very hungry starts lowering the chance
-    // square rooting the value makes the numbers drop off faster when below 1
-    healing_factor *= std::sqrt( static_cast<float>( get_stored_kcal() ) / static_cast<float>
-                                 ( max_stored_kcal() ) );
-    // Similar for thirst - starts at very thirsty, drops to 0 at parched
-    healing_factor *= 1.0f - clamp( 1.0f * ( get_thirst() - thirst_levels::very_thirsty ) /
-                                    +thirst_levels::parched, 0.0f, 1.0f );
-
-    // Mutagenic healing factor!
-    bool needs_splint = true;
-
-    healing_factor *= mutation_value( "mending_modifier" );
-
-    if( has_trait( trait_REGEN_LIZ ) ) {
-        needs_splint = false;
-    }
-
-    add_msg( m_debug, "Limb mend healing factor: %.2f", healing_factor );
-    if( healing_factor <= 0.0f ) {
-        // The section below assumes positive healing rate
-        return;
-    }
-
-    for( const bodypart_id &bp : get_all_body_parts() ) {
-        const bool broken = is_limb_broken( bp );
-        if( !broken ) {
-            continue;
-        }
-
-        if( needs_splint && !worn_with_flag( flag_SPLINT, bp ) ) {
-            continue;
-        }
-
-        const time_duration dur_inc = 1_turns * roll_remainder( rate_multiplier * healing_factor );
-        auto &eff = get_effect( effect_mending, bp->token );
-        if( eff.is_null() ) {
-            add_effect( effect_mending, dur_inc, bp->token );
-            continue;
-        }
-
-        eff.set_duration( eff.get_duration() + dur_inc );
-
-        if( eff.get_duration() >= eff.get_max_duration() ) {
-            set_part_hp_cur( bp, 1 );
-            remove_effect( effect_mending, bp->token );
-            g->events().send<event_type::broken_bone_mends>( getID(), bp->token );
-            //~ %s is bodypart
-            add_msg_if_player( m_good, _( "Your %s has started to mend!" ),
-                               body_part_name( bp ) );
-        }
-    }
-}
-
 void Character::sound_hallu()
 {
     // Random 'dangerous' sound from a random direction
diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp
index fc95f1696d8..307b9a8834f 100644
--- a/tests/player_helpers.cpp
+++ b/tests/player_helpers.cpp
@@ -98,6 +98,9 @@ void clear_character( player &dummy, bool debug_storage )
     dummy.set_stamina( dummy.get_stamina_max() );
     dummy.set_movement_mode( CMM_WALK );
 
+    // Set HP to max here and also later, for disabled/broken limbs
+    dummy.set_all_parts_hp_to_max();
+
     // Make sure we don't carry around weird effects.
     dummy.clear_effects(); // mark effects for removal
     dummy.process_effects(); // actually remove them
-- 
2.42.0