From cf861efefd139c016317f8eba86bf69fa38c9833 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Feb 2026 10:39:03 -0500 Subject: [PATCH] Mech Controls Rework (#21762) This PR reworks the controls for Mechs to now operate under "Battletech-Style" controls. Where the W and S keys are instead Forward and Reverse, while A and D are turn-left and turn-right respectively. This control scheme is generally significantly more playable for mechs, since mechs can only interact with objects or fire weapons in a forward-facing arc. Mechs were previously incapable of both repositioning and fighting at the same time, while with this update a mech can now face an enemy, fire upon them, and either move closer to them or further away. Mechs also now have reverse speed characteristics based on their chassis selection. Bipedal legs generally have the worst reverse speed. Quad legs have significantly better reverse handling, and Treads have no reverse speed reduction at all (with the fun caveat that they suck at turning). I have also fixed the issue of mechs feeling like they were "too easy to mobility kill". It turned out that mechs were hardcoded to be mobility killed when they took only 45 points of damage, regardless of how tough their legs were. I have reworked this to instead be a linear decrease in movement speed, based on the damage taken ratio of the legs. Larger and tougher legs are naturally more resistant to being impeded by mobility damage. I have actually tested this PR, here's it in action! NOW WITH STRAFING https://github.com/user-attachments/assets/3123fed8-ec22-4118-8a6a-7cbfe45e6667 --------- Signed-off-by: VMSolidus Co-authored-by: Cody Brittain <1779662+Generalcamo@users.noreply.github.com> --- code/__DEFINES/dcs/signals.dm | 8 ++ code/modules/heavy_vehicle/components/legs.dm | 20 +++ code/modules/heavy_vehicle/mech_helpers.dm | 80 +++++++++++- .../modules/heavy_vehicle/mech_interaction.dm | 120 +++++++++++++----- code/modules/heavy_vehicle/mecha.dm | 9 ++ .../modules/heavy_vehicle/premade/hoverpod.dm | 4 + code/modules/heavy_vehicle/premade/misc.dm | 5 + .../mob/living/carbon/human/inventory.dm | 5 + code/modules/mob/mob_movement.dm | 8 ++ .../hellfirejag - mech control rework.yml | 6 + 10 files changed, 226 insertions(+), 39 deletions(-) create mode 100644 html/changelogs/hellfirejag - mech control rework.yml diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 063905eea04..fe26af36488 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -79,4 +79,12 @@ /// Raised on a mob to check it's psi-sensitivity rating. This is not the same thing as checking if someone is psionic, but psionic people have an innate bonus to the check. #define COMSIG_PSI_CHECK_SENSITIVITY "psi_check_sensitivity" +// directional input signals. +// TODO: Please rework these if you are porting actual keybindings. +/// Raised on a mob when receiving a "Northeast" input key, typically the 'e' key. +#define COMSIG_INPUT_KEY_QUICK_EQUIP "quick-equip_key_pressed" + +/// Raised on a mob when receiving a "Northwest" input key, typically the 'q' key. +#define COMSIG_INPUT_KEY_DROP "drop_key_pressed" + /*******Component Specific Signals*******/ diff --git a/code/modules/heavy_vehicle/components/legs.dm b/code/modules/heavy_vehicle/components/legs.dm index 1b4e2b25718..657abacb33f 100644 --- a/code/modules/heavy_vehicle/components/legs.dm +++ b/code/modules/heavy_vehicle/components/legs.dm @@ -3,8 +3,28 @@ center_of_mass = list("x"=24, "y"=4) icon_state = "loader_legs" power_use = 75 + /// Movement delay added when moving in any direction. var/move_delay = 5 + + /// Movement delay added when turning, cumulative with move_delay. var/turn_delay = 5 + + /// Extra movement delay added when using reverse throttle. + var/reverse_delay = 10 + + /// Extra movement delay added when strafing. + var/strafe_delay_modifier = 1.5 + + /// Whether or not the legs allow strafing at all. + var/can_strafe = TRUE + + /** + * Extra movement delay added based on the ratio of the legs current damage to its maximum damage. + * IE: If your max is 100, and you've taken 50 damage to the legs, this delay should come out to +5. + * At 0 damage, the delay from this is also 0. So it linearly scales with leg damage. + */ + var/damaged_delay = 10 + var/obj/item/robot_parts/robot_component/actuator/motivator var/mech_turn_sound = 'sound/mecha/mechturn.ogg' var/mech_step_sound = 'sound/mecha/mechstep.ogg' diff --git a/code/modules/heavy_vehicle/mech_helpers.dm b/code/modules/heavy_vehicle/mech_helpers.dm index 6356742d8c2..2fe377bc896 100644 --- a/code/modules/heavy_vehicle/mech_helpers.dm +++ b/code/modules/heavy_vehicle/mech_helpers.dm @@ -1,5 +1,4 @@ /mob/living/heavy_vehicle/proc/can_move(var/mob/user) - . = 0 if(world.time < next_mecha_move) return @@ -12,14 +11,12 @@ next_mecha_move = world.time + 3 // Just to stop them from getting spammed with messages. return - if(!legs.motivator || legs.total_damage > 45) + if(!legs.motivator || legs.total_damage != 0 && legs.total_damage >= legs.max_damage) if(user) - to_chat(user, SPAN_WARNING("Your motivators are damaged! You can't move!")) + to_chat(user, SPAN_WARNING("Your motivators are destroyed! You can't move!")) next_mecha_move = world.time + 15 return - next_mecha_move = world.time + (incorporeal_move ? legs.move_delay / 2 : legs.move_delay) - if(maintenance_protocols) if(user) to_chat(user, SPAN_WARNING("Maintenance protocols are in effect.")) @@ -31,6 +28,79 @@ to_chat(user, SPAN_WARNING("The power indicator flashes briefly.")) return + next_mecha_move = world.time + (incorporeal_move ? legs.move_delay / 2 : legs.move_delay) + (legs.damaged_delay * (legs.total_damage / legs.max_damage)) + return TRUE + +/mob/living/heavy_vehicle/proc/can_turn(var/mob/user) + if(world.time < next_mecha_turn) + return + + if(incapacitated() || (user && user.incapacitated()) || lockdown) + return + + if(!legs) + if(user) + to_chat(user, SPAN_WARNING("\The [src] has no means of propulsion!")) + next_mecha_turn = world.time + 3 // Just to stop them from getting spammed with messages. + return + + if(!legs.motivator || legs.total_damage != 0 && legs.total_damage >= legs.max_damage) + if(user) + to_chat(user, SPAN_WARNING("Your motivators are destroyed! You can't turn!")) + next_mecha_turn = world.time + 15 + return + + if(maintenance_protocols) + if(user) + to_chat(user, SPAN_WARNING("Maintenance protocols are in effect.")) + return + + var/obj/item/cell/C = get_cell() + if(!C || !C.check_charge(legs.power_use * CELLRATE)) + if(user) + to_chat(user, SPAN_WARNING("The power indicator flashes briefly.")) + return + + next_mecha_turn = world.time + legs.turn_delay + (legs.damaged_delay * (legs.total_damage / legs.max_damage)) + return TRUE + +/mob/living/heavy_vehicle/proc/can_strafe(var/mob/user) + if (world.time < next_mecha_move) + return + + if(incapacitated() || (user && user.incapacitated()) || lockdown) + return + + if(!legs) + if(user) + to_chat(user, SPAN_WARNING("\The [src] has no means of propulsion!")) + next_mecha_move = world.time + 3 // Just to stop them from getting spammed with messages. + return + + if(!legs.can_strafe) + if(user) + to_chat(user, SPAN_WARNING("Your motivators are not capable of strafing!")) + next_mecha_move = world.time + 15 + return + + if(!legs.motivator || legs.total_damage != 0 && legs.total_damage >= legs.max_damage) + if(user) + to_chat(user, SPAN_WARNING("Your motivators are destroyed! You can't strafe!")) + next_mecha_move = world.time + 15 + return + + if(maintenance_protocols) + if(user) + to_chat(user, SPAN_WARNING("Maintenance protocols are in effect.")) + return + + var/obj/item/cell/C = get_cell() + if(!C || !C.check_charge(legs.power_use * CELLRATE)) + if(user) + to_chat(user, SPAN_WARNING("The power indicator flashes briefly.")) + return + + next_mecha_move = world.time + ((legs.move_delay + (legs.damaged_delay * (legs.total_damage / legs.max_damage))) * legs.strafe_delay_modifier) return TRUE /mob/living/heavy_vehicle/get_standard_pixel_x() diff --git a/code/modules/heavy_vehicle/mech_interaction.dm b/code/modules/heavy_vehicle/mech_interaction.dm index b7f251cae55..8698fb1ad7a 100644 --- a/code/modules/heavy_vehicle/mech_interaction.dm +++ b/code/modules/heavy_vehicle/mech_interaction.dm @@ -228,6 +228,8 @@ user.forceMove(src) LAZYDISTINCTADD(pilots, user) RegisterSignal(user, COMSIG_MOB_FACEDIR, PROC_REF(handle_user_turn)) + RegisterSignal(user, COMSIG_INPUT_KEY_QUICK_EQUIP, PROC_REF(strafe_left)) + RegisterSignal(user, COMSIG_INPUT_KEY_DROP, PROC_REF(strafe_right)) playsound(src, 'sound/machines/windowdoor.ogg', 50, 1) if(user.client) user.client.screen |= hud_elements LAZYDISTINCTADD(user.additional_vision_handlers, src) @@ -260,55 +262,105 @@ set_intent(I_HURT) LAZYREMOVE(pilots, user) UnregisterSignal(user, COMSIG_MOB_FACEDIR) + UnregisterSignal(user, COMSIG_INPUT_KEY_QUICK_EQUIP) + UnregisterSignal(user, COMSIG_INPUT_KEY_DROP) UNSETEMPTY(pilots) /mob/living/heavy_vehicle/proc/handle_user_turn(var/mob/living/user, var/direction) SIGNAL_HANDLER INVOKE_ASYNC(src, TYPE_PROC_REF(/atom, relaymove), user, direction, TRUE) +/mob/living/heavy_vehicle/proc/strafe_left(var/mob/user, var/cancelled) + SIGNAL_HANDLER + // Stop the pilot from attempting to drop the item in their hands, we're replacing it with a strafe input. + *cancelled = TRUE + strafe_move(user, angle2dir(dir2angle(dir) + 90)) + +/mob/living/heavy_vehicle/proc/strafe_right(var/mob/user, var/cancelled) + SIGNAL_HANDLER + // Stop the pilot from attempting to drop the item in their hands, we're replacing it with a strafe input. + *cancelled = TRUE + strafe_move(user, angle2dir(dir2angle(dir) + 270)) + /mob/living/heavy_vehicle/relaymove(mob/living/user, direction, var/turn_only = FALSE) . = ..() - if(!can_move(user)) - return - if(hallucination >= EMP_MOVE_DISRUPT && prob(30)) direction = pick(GLOB.cardinals) - var/do_strafe = !isnull(user.facing_dir) && (legs.turn_delay <= legs.move_delay) - if(!do_strafe && dir != direction) + // Convert keyboard inputs to Battletech-style controls. + switch (direction) + if (NORTH) // "Throttle Forwards" + throttle_move(user, dir, FALSE) + if (SOUTH) // "Throttle Reverse" + throttle_move(user, angle2dir(dir2angle(dir) + 180), TRUE) + if (EAST) // "Turn Right" + rotate_by_angle(user, angle2dir(dir2angle(dir) + 90)) + if (WEST) // "Turn Left" + rotate_by_angle(user, angle2dir(dir2angle(dir) + 270)) + +/mob/living/heavy_vehicle/proc/throttle_move(mob/living/user, direction, reverse) + if (!legs || !can_move(user)) + return + + // Get the tile in the direction. + var/turf/target_loc = get_step(src, direction) + if(!legs.can_move_on(loc, target_loc)) + return + + if (reverse) + next_mecha_move += legs.reverse_delay + + // Then send a move command + if(incorporeal_move) + if(legs.mech_step_sound) + playsound(src.loc,legs.mech_step_sound,40,1) use_cell_power(legs.power_use * CELLRATE) - if(legs && legs.mech_turn_sound) - playsound(src.loc,legs.mech_turn_sound,40,1) - if(world.time + legs.turn_delay > next_mecha_move) - next_mecha_move = world.time + legs.turn_delay - set_dir(direction) - for(var/mob/pilot in pilots) - pilot.set_dir(direction) - if(istype(hardpoints[HARDPOINT_BACK], /obj/item/mecha_equipment/shield)) - var/obj/item/mecha_equipment/shield/S = hardpoints[HARDPOINT_BACK] - if(S.aura) - S.aura.dir = direction - if(S.aura.dir == NORTH) - S.aura.layer = MOB_LAYER - else - S.aura.layer = ABOVE_HUMAN_LAYER - update_icon() + user.client.Process_Incorpmove(direction, src) + else + Move(target_loc, direction, 0, FALSE) - if(!turn_only) - var/turf/target_loc = get_step(src, direction) - if(!legs.can_move_on(loc, target_loc)) - return - if(incorporeal_move) - if(legs && legs.mech_step_sound) - playsound(src.loc,legs.mech_step_sound,40,1) - use_cell_power(legs.power_use * CELLRATE) - user.client.Process_Incorpmove(direction, src) - else - var/new_direction = do_strafe ? user.facing_dir || direction : direction - Move(target_loc, new_direction) +/mob/living/heavy_vehicle/proc/strafe_move(mob/user, direction) + if (!legs || !can_strafe(user)) + return -/mob/living/heavy_vehicle/Move() + // Get the tile in the direction. + var/turf/target_loc = get_step(src, direction) + if(!legs.can_move_on(loc, target_loc)) + return + + // Then send a move command + if(incorporeal_move) + if(legs.mech_step_sound) + playsound(src.loc,legs.mech_step_sound,40,1) + use_cell_power(legs.power_use * CELLRATE) + user.client.Process_Incorpmove(direction, src) + else + Move(target_loc, direction, 0, FALSE) + +/mob/living/heavy_vehicle/proc/rotate_by_angle(mob/living/user, direction) + if (!legs || !can_turn(user)) + return + + use_cell_power(legs.power_use * CELLRATE) + if(legs && legs.mech_turn_sound) + playsound(src.loc,legs.mech_turn_sound,40,1) + + set_dir(direction) + for(var/mob/pilot in pilots) + pilot.set_dir(direction) + if(istype(hardpoints[HARDPOINT_BACK], /obj/item/mecha_equipment/shield)) + var/obj/item/mecha_equipment/shield/S = hardpoints[HARDPOINT_BACK] + if(S.aura) + S.aura.dir = direction + if(S.aura.dir == NORTH) + S.aura.layer = MOB_LAYER + else + S.aura.layer = ABOVE_HUMAN_LAYER + update_icon() + Move(src.loc, direction, 0, TRUE) + +/mob/living/heavy_vehicle/Move(atom/newloc, direct, glide_size_override = 0, update_dir = TRUE) . = ..() if(. && !istype(loc, /turf/space)) if(legs) diff --git a/code/modules/heavy_vehicle/mecha.dm b/code/modules/heavy_vehicle/mecha.dm index f88ec5c4d7e..2fb44bb4330 100644 --- a/code/modules/heavy_vehicle/mecha.dm +++ b/code/modules/heavy_vehicle/mecha.dm @@ -76,7 +76,16 @@ var/use_air = FALSE // Interface stuff. + + /// The next world tick that the mech has to wait for before it can change its Throttle (forward and backward movement). var/next_mecha_move = 0 + + /// The next world tick that the mech has to wait for before it can turn. + var/next_mecha_turn = 0 + + /// The next world tick that the mech has to wait for before it can strafe. + var/next_mecha_strafe = 0 + var/list/hud_elements = list() var/list/hardpoint_hud_elements = list() var/atom/movable/screen/mecha/health/hud_health diff --git a/code/modules/heavy_vehicle/premade/hoverpod.dm b/code/modules/heavy_vehicle/premade/hoverpod.dm index 80c13625529..4c21fe4da72 100644 --- a/code/modules/heavy_vehicle/premade/hoverpod.dm +++ b/code/modules/heavy_vehicle/premade/hoverpod.dm @@ -8,6 +8,8 @@ max_damage = 150 move_delay = 5 turn_delay = 2 + strafe_delay_modifier = 1 // No delay on strafing. + reverse_delay = 10 power_use = 2500 trample_damage = 0 hover = TRUE @@ -20,4 +22,6 @@ max_damage = 75 move_delay = 2 turn_delay = 2 + strafe_delay_modifier = 1 // No delay on strafing. + reverse_delay = 5 power_use = 2000 diff --git a/code/modules/heavy_vehicle/premade/misc.dm b/code/modules/heavy_vehicle/premade/misc.dm index cad77053eb8..30c08fb845c 100644 --- a/code/modules/heavy_vehicle/premade/misc.dm +++ b/code/modules/heavy_vehicle/premade/misc.dm @@ -6,6 +6,8 @@ max_damage = 160 move_delay = 4 turn_delay = 1 + reverse_delay = 2 // Faster than usual reverse for legs + strafe_delay_modifier = 1.2 // Faster than usual strafing power_use = 1250 trample_damage = 10 @@ -17,6 +19,7 @@ max_damage = 250 move_delay = 5 turn_delay = 1 + strafe_delay_modifier = 1.2 // Faster than usual strafing power_use = 3500 trample_damage = 25 @@ -28,7 +31,9 @@ max_damage = 450 move_delay = 2 //Its fast turn_delay = 7 + reverse_delay = 0 // Uniquely the only chassis without a reverse delay. power_use = 3500 color = COLOR_WHITE mech_step_sound = 'sound/mecha/tanktread.ogg' trample_damage = 25 + can_strafe = FALSE diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index ea0dddcfdba..8f97826f736 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -7,6 +7,11 @@ This saves us from having to call add_fingerprint() any time something is put in set name = "quick-equip" set hidden = 1 + var/cancelled = FALSE + SEND_SIGNAL(src, COMSIG_INPUT_KEY_QUICK_EQUIP, &cancelled) + if (cancelled) + return + if(ishuman(src)) var/mob/living/carbon/human/H = src var/obj/item/I = H.get_active_hand() diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 3bbaa730632..a566eb1b1e3 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -52,6 +52,9 @@ diagonal_action(SOUTHWEST) /client/proc/diagonal_action(direction) + if (!mob) + return + switch(client_dir(direction, 1)) if(NORTHEAST) swap_hand() @@ -67,6 +70,11 @@ to_chat(usr, SPAN_WARNING("This mob type cannot throw items.")) return if(NORTHWEST) + var/cancelled = FALSE + SEND_SIGNAL(mob, COMSIG_INPUT_KEY_DROP, &cancelled) + if (cancelled) + return + if(iscarbon(usr)) var/mob/living/carbon/C = usr if(!C.get_active_hand()) diff --git a/html/changelogs/hellfirejag - mech control rework.yml b/html/changelogs/hellfirejag - mech control rework.yml new file mode 100644 index 00000000000..afc225aae85 --- /dev/null +++ b/html/changelogs/hellfirejag - mech control rework.yml @@ -0,0 +1,6 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "Reworked mech controls to be Battletech-style. W and S are Forward and Reverse throttle, while A and D rotate the mech left and right. Mechs have different forward and reverse speeds based on their chassis. When reversing, your faced direction doesn't change." + - rscadd: "Reworked leg damage for mechs. A mech's movement speeds are reduced by up to a certain amount based on their percentage of damage taken. Larger legs can generally take more of a beating before they slow down too much." + - rscadd: "Mechs can now strafe left and right with the Q and E keys respectively."