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 <evilexecutive@gmail.com>
Co-authored-by: Cody Brittain <1779662+Generalcamo@users.noreply.github.com>
This commit is contained in:
VMSolidus
2026-02-17 15:39:03 +00:00
committed by GitHub
co-authored by Cody Brittain
parent 2a2d0798b9
commit cf861efefd
10 changed files with 226 additions and 39 deletions
@@ -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'
+75 -5
View File
@@ -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()
+86 -34
View File
@@ -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)
+9
View File
@@ -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
@@ -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
@@ -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
@@ -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()
+8
View File
@@ -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())