From 72fe8e00cb21c1e4b9257d1351b56bb705cb608f Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:35:46 -0500 Subject: [PATCH] Dismounting someone from a Pony (or Leapers) requires shoving and is not guaranteed (#95517) ## About The Pull Request Ponies can no longer be one-click dismounted like typical mounts Instead when you shove a pony (or its rider), there is a chance based on multiple factors (is already staggered, has stamina damage, is wounded) that you manage to remove the rider Successfully disarming a rider this way will knock them down for a very short duration This save behavior was applied to the Leaper, though as Leapers cannot be staggered and are significantly larger, the chance of a successful dismount is far lower ## Why It's Good For The Game Being able to be spam clicked to lose your mount is kind of unfortunate However, being completely immune to dismount is also not ideal So as a middle ground, a little stability minigame makes sense to me - it gives both sides agency in avoiding or pursuing a dismount. The rider can keep a distance to recover stability while the attackers can use weapons (even batons or disablers) to make it easier to remove the rider. ## Changelog :cl: Melbert balance: You can no longer dismount riders from ponies with a single click. Instead, there is a chance to dismount the rider on shoving the pony OR the rider, based on multiple factors (such as stamina + physical damage and stagger) balance: The Leaper mount also has this "disarm to dismount" mechanic, though it is significantly more difficult as they are much sturdier beasts /:cl: --- code/__DEFINES/mobs.dm | 8 ++ code/datums/components/riding/riding.dm | 119 ++++++++++++++++-- code/datums/components/riding/riding_mob.dm | 3 +- .../mob/living/basic/farm_animals/pony.dm | 4 + 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 9d927697ceb..64f26c02b0a 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -969,6 +969,14 @@ GLOBAL_LIST_INIT(layers_to_offset, list( /// Possible value of [/atom/movable/buckle_dir]. If set to a different (positive-or-zero) value than this, the buckling thing will force a dir on the buckled. #define BUCKLE_MATCH_DIR -1 +// Defines for [/datum/component/riding/var/other_unbuckle] +/// Other mobs cannot force riders to unbuckle in any means +#define CANNOT_FORCE_UNBUCKLE 0 +/// Other mobs can force riders to unbuckle by simply clicking on the parent +#define CAN_FORCE_UNBUCKLE 1 +/// Other mobs can force riders to unbuckle by disarming the parent or the rider minimum twice +#define CAN_DISARM_UNBUCKLE 2 + // Flags for fully_heal(). /// Special flag that means this heal is an admin heal and goes above and beyond diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm index 8e5f1eab52f..56faa2982d3 100644 --- a/code/datums/components/riding/riding.dm +++ b/code/datums/components/riding/riding.dm @@ -30,8 +30,8 @@ var/list/rider_traits = list(TRAIT_NO_FLOATING_ANIM) /// We don't need roads where we're going if this is TRUE, allow normal movement in space tiles var/override_allow_spacemove = FALSE - /// can anyone other than the rider unbuckle the rider? - var/can_force_unbuckle = TRUE + /// determines behavior for other mobs trying to unbuckle riders + var/other_unbuckle = CAN_FORCE_UNBUCKLE /// Like last_bumped for mobs, but for vehicles. Exists to allow the door bump cooldown while also passing door openings through Bumped() var/vehicle_last_bumped = 0 @@ -67,8 +67,13 @@ RegisterSignals(parent, GLOB.movement_type_addtrait_signals, PROC_REF(on_movement_type_trait_gain)) RegisterSignals(parent, GLOB.movement_type_removetrait_signals, PROC_REF(on_movement_type_trait_loss)) RegisterSignal(parent, COMSIG_SUPERMATTER_CONSUMED, PROC_REF(on_entered_supermatter)) - if(!can_force_unbuckle) - RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(force_unbuckle)) + switch(other_unbuckle) + if(CAN_FORCE_UNBUCKLE) + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(block_unbuckle)) + if(CAN_DISARM_UNBUCKLE) + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(block_unbuckle)) + if(isliving(parent)) + RegisterSignal(parent, COMSIG_LIVING_DISARM_HIT, PROC_REF(parent_disarmed)) /// This proc is called when a rider unbuckles, whether they chose to or not. If there's no more riders, this will be the riding component's death knell. /datum/component/riding/proc/vehicle_mob_unbuckle(datum/source, mob/living/rider, force = FALSE) @@ -88,6 +93,7 @@ unequip_buckle_inhands(rider) rider.updating_glide_size = TRUE UnregisterSignal(rider, COMSIG_LIVING_TRY_PULL) + UnregisterSignal(rider, COMSIG_LIVING_DISARM_HIT) for (var/trait in GLOB.movement_type_trait_to_flag) if (HAS_TRAIT(parent, trait)) REMOVE_TRAIT(rider, trait, REF(src)) @@ -105,6 +111,8 @@ if(rider.pulling == parent) rider.stop_pulling() RegisterSignal(rider, COMSIG_LIVING_TRY_PULL, PROC_REF(on_rider_try_pull)) + if(other_unbuckle == CAN_DISARM_UNBUCKLE) + RegisterSignal(rider, COMSIG_LIVING_DISARM_HIT, PROC_REF(rider_disarmed)) for (var/trait in GLOB.movement_type_trait_to_flag) if (HAS_TRAIT(parent, trait)) @@ -317,13 +325,110 @@ for (var/mob/rider in movable_parent.buckled_mobs) REMOVE_TRAIT(rider, trait, REF(src)) -/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter) +/// Block attempts to unbuckle by direct click in certain conditions +/datum/component/riding/proc/block_unbuckle(atom/movable/source, mob/living/unbuckler) SIGNAL_HANDLER - if((living_hitter in source.buckled_mobs)) - return + if(!source.has_buckled_mobs() || (unbuckler in source.buckled_mobs)) + return NONE + + switch(other_unbuckle) + if(CANNOT_FORCE_UNBUCKLE) + to_chat(unbuckler, span_warning("You can't dismount [source]'s rider[length(source.buckled_mobs) == 1 ? "" : "s"]!")) + if(CAN_DISARM_UNBUCKLE) + parent_disarmed(source, unbuckler) + return COMPONENT_CANCEL_ATTACK_CHAIN +/// If our parent is a living mob it can be disarmed which may trigger a forced unbuckle +/datum/component/riding/proc/parent_disarmed(mob/living/source, mob/living/disarmer) + SIGNAL_HANDLER + if(!source.has_buckled_mobs() || (disarmer in source.buckled_mobs)) + return + var/mob/living/throwing = pick(source.buckled_mobs) + var/disarm_chance = get_disarm_chance(source) + get_disarm_chance(disarmer) + if(!prob(disarm_chance)) + if(disarm_chance > 0) + source.visible_message( + span_warning("[disarmer] shoves [source] around, trying to throw off [source.p_their()] rider[length(source.buckled_mobs) == 1 ? "" : "s"]!"), + span_warning("[disarmer] shoves you around, trying to throw off your rider[length(source.buckled_mobs) == 1 ? "" : "s"]!"), + vision_distance = COMBAT_MESSAGE_RANGE, + ) + if(source.is_blind()) + to_chat(source, span_warning("Someone shoves you around, trying to throw off your rider[length(source.buckled_mobs) == 1 ? "" : "s"]!")) + if(!disarmer.is_blind()) + switch(disarm_chance) + if(50 to INFINITY) + to_chat(disarmer, span_warning("[source] barely holds stable as you shove them around! Keep at it!")) + if(25 to 50) + to_chat(disarmer, span_warning("[source] holds stable as you shove them around! Weakening [source.p_their()] stability would help...")) + if(-INFINITY to 25) + to_chat(disarmer, span_warning("[source] effortlessly holds stable as you shove them around! You'd need to weaken [source.p_their()] stability...!")) + return + source.visible_message( + span_warning("As [disarmer] shoves [source] around, [throwing] is thrown from [parent]!"), + span_warning("As [disarmer] shoves you around, [throwing] is thrown from you!"), + ) + if(source.is_blind()) + to_chat(source, span_warning("As someone shoves you around, you feel [length(source.buckled_mobs) == 1 ? "your rider" : "one of your riders"] get thrown from you!")) + + source.unbuckle_mob(throwing, force = TRUE) + throwing.Knockdown(throwing.has_status_effect(/datum/status_effect/staggered) ? SHOVE_KNOCKDOWN_COLLATERAL : SHOVE_KNOCKDOWN_HUMAN) + +/// If our rider is disarmed, they may be thrown from the vehicle depending on their and the disarmer's stats +/datum/component/riding/proc/rider_disarmed(mob/living/rider, mob/living/disarmer) + SIGNAL_HANDLER + var/disarm_chance = get_disarm_chance(parent) + get_disarm_chance(rider) + if(!prob(disarm_chance)) + if(disarm_chance > 0) + rider.visible_message( + span_warning("[disarmer] shoves [rider] around, trying to throw [rider.p_them()] off [parent]!"), + span_warning("As [disarmer] shoves you around, you [(disarm_chance >= 50 ? "barely" : (disarm_chance >= 25 ? "" : "effortlessly"))] manage to hold on to [parent]!"), + vision_distance = COMBAT_MESSAGE_RANGE, + ) + if(rider.is_blind()) + to_chat(rider, span_warning("As someone shoves you around, you [(disarm_chance >= 50 ? "barely" : (disarm_chance >= 25 ? "" : "effortlessly"))] manage to hold on to [parent]!")) + if(!disarmer.is_blind()) + switch(disarm_chance) + if(50 to INFINITY) + to_chat(disarmer, span_warning("[rider] barely holds on to [parent] as you shove them around! Keep at it!")) + if(25 to 50) + to_chat(disarmer, span_warning("[rider] holds on to [parent] as you shove them around! Weakening [rider.p_their()] [pick("balance", "grip", "hold")] would help...")) + if(-INFINITY to 25) + to_chat(disarmer, span_warning("[rider] effortlessly holds on to [parent] as you shove them around! You'd need to weaken [rider.p_their()] [pick("balance", "grip", "hold")]...!")) + return + + rider.visible_message( + span_warning("As [disarmer] shoves [rider] around, [rider.p_they()] lose [rider.p_their()] [pick("balance", "grip", "hold")] and fall off [parent]!"), + span_warning("As [disarmer] shoves you around, you lose your [pick("balance", "grip", "hold")] and fall off [parent]!") + ) + if(rider.is_blind()) + to_chat(rider, span_warning("As someone shoves you around, you lose your [pick("balance", "grip", "hold")] and fall off [parent]!")) + + var/atom/movable/riding = parent + riding.unbuckle_mob(rider, force = TRUE) + if(rider.has_status_effect(/datum/status_effect/staggered)) + rider.Knockdown(2 SECONDS) + +/// Calculates chance to be thrown off the mount based on mob state +/datum/component/riding/proc/get_disarm_chance(mob/living/disarmed) + if(!isliving(disarmed)) + return 0 + + var/staminaloss = round(disarmed.get_stamina_loss(), 0.1) + var/healthpercent = round(1 - (disarmed.health / disarmed.maxHealth), 0.01) + + var/chance = 0 + + chance += disarmed.has_status_effect(/datum/status_effect/staggered) ? 25 : 0 + chance += INCAPACITATED_IGNORING(disarmed, INCAPABLE_STASIS) ? 75 : 0 + chance += staminaloss >= 10 ? (staminaloss * 0.5) : 0 + chance += healthpercent >= 0.25 ? ((disarmed.health / disarmed.maxHealth) * 50) : 0 + + chance -= max(0, disarmed.mob_size - MOB_SIZE_HUMAN) * 12.5 + + return chance + /// When we touch a crystal, kill everything inside us /datum/component/riding/proc/on_entered_supermatter(atom/movable/ridden, atom/movable/supermatter) SIGNAL_HANDLER diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm index dcfcd0d84d5..bc457829b7a 100644 --- a/code/datums/components/riding/riding_mob.dm +++ b/code/datums/components/riding/riding_mob.dm @@ -392,6 +392,7 @@ ) /datum/component/riding/creature/pony + other_unbuckle = CAN_DISARM_UNBUCKLE vehicle_move_delay = 1.5 COOLDOWN_DECLARE(pony_trot_cooldown) @@ -608,7 +609,7 @@ ) /datum/component/riding/creature/leaper - can_force_unbuckle = FALSE + other_unbuckle = CAN_DISARM_UNBUCKLE can_use_abilities = TRUE ride_check_flags = JUST_FRIEND_RIDERS diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm index a44d042809c..3e9ad63ad39 100644 --- a/code/modules/mob/living/basic/farm_animals/pony.dm +++ b/code/modules/mob/living/basic/farm_animals/pony.dm @@ -119,6 +119,10 @@ whinny_angrily() +/mob/living/basic/pony/get_shove_flags(mob/living/shover, obj/item/weapon) + . = ..() + . |= SHOVE_CAN_STAGGER + /datum/ai_controller/basic_controller/pony blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,