Walking Aid Component (#96294)

## About The Pull Request
So I saw someone was adding a limping quirk in
https://github.com/tgstation/tgstation/pull/96200 and I had tried to do
the same back in https://github.com/tgstation/tgstation/pull/71470 a few
years ago. The codebase has evolved enough to support this behavior that
was not present initially during my first attempt. One of the big
features of my old PR was to add cane-like behavior to a bunch of other
pole objects.

This PR introduces a new modular `/datum/component/walking_aid`
component, moving canes/crutches away from hardcoded object behavior.
This component is now attached to the following items:
- Brooms
- Mops
- Staves
- Scythes
- Spears
- Canes
- Crutches

There are also some notable changes I made to how things work, detailed
below:
- Walking aid objects must be held on the same side as the affected leg
(if your right leg is missing, you need to be holding crutches on your
right arm)
- Objects lose their ability as a walking aid while being wielded with
two hands (ex. spears)
- Limping/Limbless are now both affected by a walking aid, where before
only crutches helped assist limbless mobs
- "Walking Aid" examine tag when the walking aid object is examined

All walking aids (except crutches) reduce limbless slowdown by 40%, and
completely ignore any limping penalties. Crutches reduce the limbless
slowdown by 60% and are much faster.

## Why It's Good For The Game
1. Mah Immersion
2. More modular code
3. Examine tags are peak
4. Assistants can now justify carrying spears as medical equipment
5. Cane/Poles should help with limblessness, but not as helpful as
crutches

While I wanted pole items to offer mobility support, they needed to
remain less effective than dedicated medical crutches. During testing, I
found that a 40% reduction is the point where the benefit becomes
noticeable.

## Changelog
🆑
add: Pole objects like mops, brooms, staves, scythes, and spears can now
be held in a hand to act as walking aids, mitigating limping from
fractures and reducing missing-leg slowdowns.
balance: Walking aids must be held in the hand on the same side as the
injured or missing leg to provide support, and lose their effectiveness
if actively wielded with two hands.
balance: Objects with the walking aid reduce limbless slowdown by 40%,
while dedicated medical crutches reduce it by 60%.
refactor: Refactor cane and crutch code logic into a modular walking aid
component
/🆑
This commit is contained in:
Tim
2026-06-08 16:13:05 +02:00
committed by GitHub
parent 7fd03e1d56
commit 5c16d8d75e
16 changed files with 224 additions and 59 deletions
@@ -167,7 +167,7 @@
/// Return to skip oxyloss and similar effects from blood level
#define HANDLE_BLOOD_NO_OXYLOSS (1<<2)
/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced) iodk where it should go
/// from /datum/status_effect/limp/proc/check_step(mob/whocares, OldLoc, Dir, forced): (var/obj/item/bodypart/limping_leg)
#define COMSIG_CARBON_LIMPING "mob_limp_check"
#define COMPONENT_CANCEL_LIMP (1<<0)
+161
View File
@@ -0,0 +1,161 @@
/**
* Walking Aid Component
*
* Add this to an item to allow it to act as a cane (or crutch) while held.
* Items with this component will help mobs avoid limping from broken
* leg bones, and lessen the slowdown caused by missing legs.
*
* Used by canes, crutches, and pole-like items such as spears and staffs.
*/
/datum/component/walking_aid
dupe_mode = COMPONENT_DUPE_UNIQUE
/// Causes a mob to waddle (wiggle) while walking when holding this object
var/waddling = FALSE
/// If set, the parent item must have this trait for the support to function
var/required_trait
/// Weakref to the mob currently being supported
var/datum/weakref/current_user_ref
/// The amount of slowdown to reduce for a limbless leg
var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40%
/datum/component/walking_aid/Initialize(limbless_slowdown_modifier = 0.6, required_trait = null, waddling = FALSE)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
src.waddling = waddling
src.required_trait = required_trait
src.limbless_slowdown_modifier = limbless_slowdown_modifier
/datum/component/walking_aid/Destroy(force)
remove_support()
return ..()
/datum/component/walking_aid/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip))
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(get_examine_tags))
RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_WIELDED), PROC_REF(update_legs))
RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_WIELDED), PROC_REF(update_legs))
RegisterSignal(parent, SIGNAL_ADDTRAIT(required_trait), PROC_REF(update_legs))
RegisterSignal(parent, SIGNAL_REMOVETRAIT(required_trait), PROC_REF(update_legs))
/datum/component/walking_aid/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_ITEM_EQUIPPED,
COMSIG_ITEM_DROPPED,
COMSIG_ATOM_EXAMINE_TAGS,
SIGNAL_ADDTRAIT(TRAIT_WIELDED),
SIGNAL_REMOVETRAIT(TRAIT_WIELDED),
SIGNAL_ADDTRAIT(required_trait),
SIGNAL_REMOVETRAIT(required_trait),
))
remove_support()
/datum/component/walking_aid/proc/on_equip(datum/source, mob/equipper, slot)
SIGNAL_HANDLER
remove_support()
if(!(slot & ITEM_SLOT_HANDS))
return
if(!isliving(equipper))
return
apply_support(equipper)
/datum/component/walking_aid/proc/on_drop(datum/source, mob/user)
SIGNAL_HANDLER
remove_support()
/datum/component/walking_aid/proc/get_examine_tags(atom/source, mob/user, list/examine_list)
SIGNAL_HANDLER
examine_list["walking-aid"] = "It can help lessen the slowdown caused from a missing or injured leg, when held on the same side as the injury."
// Updates our leg status when wielded/unwielded a two handed walking aid like a spear
/datum/component/walking_aid/proc/update_legs(atom/source)
SIGNAL_HANDLER
var/mob/living/user = current_user_ref?.resolve()
user?.update_usable_leg_status()
/datum/component/walking_aid/proc/apply_support(mob/living/user)
if(current_user_ref)
remove_support()
current_user_ref = WEAKREF(user)
RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping))
RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown))
user.update_usable_leg_status()
if(waddling)
user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
/datum/component/walking_aid/proc/remove_support()
var/mob/living/user = current_user_ref?.resolve()
current_user_ref = null
if(isnull(user))
return
UnregisterSignal(user, list(COMSIG_CARBON_LIMPING, COMSIG_LIVING_LIMBLESS_SLOWDOWN))
user.update_usable_leg_status()
if(waddling)
REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src))
/datum/component/walking_aid/proc/is_active()
// if both hands are holding it, then it is not being used for support
if(HAS_TRAIT(parent, TRAIT_WIELDED))
return FALSE
if(isnull(required_trait))
return TRUE
return HAS_TRAIT(parent, required_trait)
/datum/component/walking_aid/proc/handle_limping(mob/living/user, obj/item/bodypart/limping_leg)
SIGNAL_HANDLER
if(!is_active())
return NONE
if(isnull(limping_leg))
return NONE
var/supported_zone = get_supported_leg_zone(user)
if(isnull(supported_zone))
return NONE
if(limping_leg.body_zone != supported_zone)
return NONE
return COMPONENT_CANCEL_LIMP
/datum/component/walking_aid/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods)
SIGNAL_HANDLER
if(!is_active())
return
if(!iscarbon(user))
return
var/mob/living/carbon/carbon_user = user
var/leg_amount = carbon_user.usable_legs
if(leg_amount >= carbon_user.default_num_legs)
return
if(!leg_amount) // someday support dual-wielding crutches but for now they are destined to waddle
return
var/supported_zone = get_supported_leg_zone(user)
if(isnull(supported_zone))
return
if(carbon_user.get_bodypart(supported_zone)) // make sure their leg is actually missing
return
slowdown_mods += limbless_slowdown_modifier
/datum/component/walking_aid/proc/get_supported_leg_zone(mob/living/user)
var/held_hand_zone = user.get_hand_zone_of_item(parent)
switch(held_hand_zone)
if(BODY_ZONE_R_ARM)
return BODY_ZONE_R_LEG
if(BODY_ZONE_L_ARM)
return BODY_ZONE_L_LEG
else
return null
+4 -1
View File
@@ -79,18 +79,21 @@
// less limping while we have determination still
var/determined_mod = owner.has_status_effect(/datum/status_effect/determined) ? 0.5 : 1
var/obj/item/bodypart/leg_about_to_limp
var/limp_chance
var/limp_slowdown
if(next_leg == left)
leg_about_to_limp = left
limp_chance = limp_chance_left
limp_slowdown = slowdown_left
next_leg = right
else
leg_about_to_limp = right
limp_chance = limp_chance_right
limp_slowdown = slowdown_right
next_leg = left
if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING) & COMPONENT_CANCEL_LIMP)
if(SEND_SIGNAL(owner, COMSIG_CARBON_LIMPING, leg_about_to_limp) & COMPONENT_CANCEL_LIMP)
return
if(prob(limp_chance * determined_mod))
+1
View File
@@ -36,6 +36,7 @@
/obj/item/mop/Initialize(mapload)
. = ..()
AddComponent(/datum/component/cleaner, mopspeed, pre_clean_callback=CALLBACK(src, PROC_REF(should_clean)), on_cleaned_callback=CALLBACK(src, PROC_REF(apply_reagents)))
AddComponent(/datum/component/walking_aid)
create_reagents(max_reagent_volume)
GLOB.janitor_devices += src
+1
View File
@@ -32,6 +32,7 @@
. = ..()
AddComponent(/datum/component/jousting)
AddComponent(/datum/component/two_handed, force_unwielded=7, force_wielded=15, icon_wielded="[base_icon_state]1")
AddComponent(/datum/component/walking_aid)
/obj/item/pitchfork/update_icon_state()
icon_state = "[base_icon_state]0"
+4
View File
@@ -337,6 +337,10 @@
var/staffcooldown = 0
var/staffwait = 30
/obj/item/godstaff/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
/obj/item/godstaff/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(SHOULD_SKIP_INTERACTION(interacting_with, src, user))
return NONE
@@ -29,6 +29,7 @@
wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
)
AddComponent(/datum/component/walking_aid)
/obj/item/pushbroom/update_icon_state()
icon_state = "[base_icon_state]0"
+15 -57
View File
@@ -13,32 +13,18 @@
custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT * 0.5)
attack_verb_continuous = list("bludgeons", "whacks", "disciplines", "thrashes")
attack_verb_simple = list("bludgeon", "whack", "discipline", "thrash")
/// The amount of slowdown to reduce for a limbless leg
var/limbless_slowdown_modifier = 0.6 // reduces slowdown by 40%
/// Does this cause waddling when held
var/causes_waddling = FALSE
/obj/item/cane/examine(mob/user, thats)
/obj/item/cane/Initialize(mapload)
. = ..()
. += span_notice("This item can be used to support your weight, preventing limping from any broken bones on your legs you may have.")
AddComponent(/datum/component/walking_aid, limbless_slowdown_modifier, get_walking_aid_required_trait(), causes_waddling)
/obj/item/cane/equipped(mob/living/user, slot, initial)
..()
if(!(slot & ITEM_SLOT_HANDS))
return
movement_support_add(user)
/obj/item/cane/dropped(mob/living/user, silent = FALSE)
. = ..()
movement_support_del(user)
/obj/item/cane/proc/movement_support_add(mob/living/user)
RegisterSignal(user, COMSIG_CARBON_LIMPING, PROC_REF(handle_limping))
return TRUE
/obj/item/cane/proc/movement_support_del(mob/living/user)
UnregisterSignal(user, list(COMSIG_CARBON_LIMPING))
return TRUE
/obj/item/cane/proc/handle_limping(mob/living/user)
SIGNAL_HANDLER
return COMPONENT_CANCEL_LIMP
/// Determines if a trait is required to be used as a walking aid (ex. foldable canes)
/obj/item/cane/proc/get_walking_aid_required_trait()
return null
/obj/item/cane/crutch
name = "medical crutch"
@@ -55,42 +41,13 @@
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 0.5)
attack_verb_continuous = list("bludgeons", "whacks", "thrashes")
attack_verb_simple = list("bludgeon", "whack", "thrash")
limbless_slowdown_modifier = 0.4 // reduces slowdown by 60%
causes_waddling = TRUE
/obj/item/cane/crutch/Initialize(mapload)
. = ..()
AddElement(/datum/element/cuffable_item)
/obj/item/cane/crutch/examine(mob/user, thats)
. = ..()
// tacked on after the cane string
. += span_notice("As a crutch, it can also help lessen the slowdown incurred by missing a leg.")
/obj/item/cane/crutch/movement_support_add(mob/living/user)
. = ..()
if(!.)
return
RegisterSignal(user, COMSIG_LIVING_LIMBLESS_SLOWDOWN, PROC_REF(handle_slowdown))
user.update_usable_leg_status()
user.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling)
/obj/item/cane/crutch/movement_support_del(mob/living/user)
. = ..()
if(!.)
return
UnregisterSignal(user, list(COMSIG_LIVING_LIMBLESS_SLOWDOWN, COMSIG_CARBON_LIMPING))
user.update_usable_leg_status()
REMOVE_TRAIT(user, TRAIT_WADDLING, REF(src))
/obj/item/cane/crutch/proc/handle_slowdown(mob/living/user, limbless_slowdown, list/slowdown_mods)
SIGNAL_HANDLER
var/leg_amount = user.usable_legs
// Don't do anything if the number is equal (or higher) to the usual.
if(leg_amount >= user.default_num_legs)
return
// If we have at least one leg and it's less than the default, reduce slowdown by 60%.
if(leg_amount && (leg_amount < user.default_num_legs))
slowdown_mods += 0.4
/obj/item/cane/crutch/wood
name = "wooden crutch"
desc = "A handmade crutch. Also makes a decent bludgeon if you need it."
@@ -124,15 +81,16 @@
)
RegisterSignal(src, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform))
/obj/item/cane/white/handle_limping(mob/living/user)
return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) ? COMPONENT_CANCEL_LIMP : NONE
// White canes only provide support while extended
/obj/item/cane/white/get_walking_aid_required_trait()
return TRAIT_TRANSFORM_ACTIVE
/*
* Signal proc for [COMSIG_TRANSFORMING_ON_TRANSFORM].
*
* Gives feedback to the user and makes it show up inhand.
*/
/obj/item/cane/white/proc/on_transform(obj/item/source, mob/user, active)
/obj/item/cane/white/proc/on_transform(obj/item/source, mob/living/user, active)
SIGNAL_HANDLER
if(user)
@@ -386,6 +386,7 @@
force_unwielded = 10, \
force_wielded = 14, \
)
AddComponent(/datum/component/walking_aid)
/obj/item/bambostaff/update_icon_state()
icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]"
@@ -414,6 +415,10 @@
attack_verb_simple = list("bludgeon", "whack", "discipline")
resistance_flags = FLAMMABLE
/obj/item/staff/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
/obj/item/staff/broom
name = "broom"
desc = "Used for sweeping, and flying into the night while cackling. Black cat not included."
@@ -39,6 +39,7 @@
RegisterSignal(soul, COMSIG_MOB_ATTACK_RANGED_SECONDARY, PROC_REF(on_secondary_attack))
RegisterSignal(src, COMSIG_ATOM_INTEGRITY_CHANGED, PROC_REF(on_integrity_change))
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
AddComponent(/datum/component/walking_aid)
/obj/item/soulscythe/examine(mob/user)
. = ..()
@@ -70,6 +70,7 @@
wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
)
AddComponent(/datum/component/walking_aid)
add_headpike_component()
update_appearance()
@@ -174,6 +174,7 @@
. = ..()
AddComponent(/datum/component/butchering, speed = 9 SECONDS, effectiveness = 105)
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5)
AddComponent(/datum/component/walking_aid)
/obj/item/scythe/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is beheading [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
@@ -340,6 +340,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
/// The icon which appears over the mob holding the item
var/shield_icon = "shield-red"
/obj/item/nullrod/staff/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
/obj/item/nullrod/staff/worn_overlays(mutable_appearance/standing, isinhands)
. = ..()
if(isinhands)
@@ -629,6 +633,7 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
force_unwielded = 14, \
force_wielded = 18, \
)
AddComponent(/datum/component/walking_aid)
/obj/item/nullrod/bostaff/update_icon_state()
icon_state = inhand_icon_state = "[base_icon_state][HAS_TRAIT(src, TRAIT_WIELDED)]"
@@ -703,6 +708,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
sharpness = SHARP_EDGED
menu_description = "A sharp pitchfork. Can be worn on the back."
/obj/item/nullrod/pitchfork/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
// Egyptian Staff - Used as a tool for making mummy wraps.
/obj/item/nullrod/egyptian
@@ -720,6 +729,11 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
attack_verb_simple = list("bash", "smack", "whack")
menu_description = "A staff. Can be used as a tool to craft exclusive egyptian items. Easily stored. Can be worn on the back."
/obj/item/nullrod/egyptian/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
// Hypertool - It does brain damage rather than normal damage.
/obj/item/nullrod/hypertool
@@ -760,6 +774,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
hitsound = 'sound/items/weapons/bladeslice.ogg'
menu_description = "A pointy spear which penetrates armor a little. Can be worn only on the belt."
/obj/item/nullrod/spear/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
// Unholy version of above, since the gamemode is dead in the water
/obj/item/brass_spear
@@ -783,6 +801,10 @@ GLOBAL_LIST_INIT(nullrod_variants, init_nullrod_variants())
attack_verb_simple = list("stab", "poke", "slash", "clock")
hitsound = 'sound/items/weapons/bladeslice.ogg'
/obj/item/brass_spear/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
// Nullblade - For when you really want to feel like rolling dice during combat
/obj/item/nullrod/nullblade
@@ -81,6 +81,7 @@ If the scythe isn't empowered when you sheath it, you take a heap of damage and
AddElement(/datum/element/nullrod_core, chaplain_spawnable = FALSE, rune_remove_line = "TO DUST WITH YE!! AWAY!!") // The implant is the actual item the chappie can select
AddComponent(/datum/component/bane, affected_biotypes = MOB_PLANT, damage_multiplier = 1.5) //also good at killing plants
AddComponent(/datum/component/butchering, speed = 3 SECONDS, effectiveness = 125)
AddComponent(/datum/component/walking_aid)
/obj/item/vorpalscythe/attack(mob/living/target, mob/living/user, list/modifiers, list/attack_modifiers)
if(ismonkey(target) && !target.mind) //Don't empower from hitting monkeys. Hit a corgi or something, I don't know.
@@ -11,6 +11,10 @@
/// If FALSE, only wizards or survivalists can use the staff to its full potential - If TRUE, anyone can
var/allow_intruder_use = FALSE
/obj/item/gun/magic/staff/Initialize(mapload)
. = ..()
AddComponent(/datum/component/walking_aid)
/obj/item/gun/magic/staff/proc/is_wizard_or_friend(mob/user)
if(!HAS_MIND_TRAIT(user, TRAIT_MAGICALLY_GIFTED) && !allow_intruder_use)
return FALSE
+1
View File
@@ -1396,6 +1396,7 @@
#include "code\datums\components\usb_port.dm"
#include "code\datums\components\vacuum.dm"
#include "code\datums\components\vision_hurting.dm"
#include "code\datums\components\walking_aid.dm"
#include "code\datums\components\wearertargeting.dm"
#include "code\datums\components\weatherannouncer.dm"
#include "code\datums\components\wet_floor.dm"