Files
VMSolidus cf861efefd 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>
2026-02-17 15:39:03 +00:00

359 lines
10 KiB
Plaintext

// Big stompy robots.
/mob/living/heavy_vehicle
name = "exosuit"
density = TRUE
anchored = TRUE
status_flags = PASSEMOTES
a_intent = I_HURT
mob_size = MOB_LARGE
mob_push_flags = ALLMOBS
can_be_buckled = FALSE
accent = ACCENT_TTS
appearance_flags = KEEP_TOGETHER | DEFAULT_APPEARANCE_FLAGS | TILE_BOUND | LONG_GLIDE
pass_flags_self = PASSVEHICLE
var/decal
var/emp_damage = 0
// Used to offset a non-32x32 icon appropriately.
var/offset_x = -8
var/offset_y = 0
var/obj/item/radio/exosuit/radio
var/obj/machinery/camera/camera
var/wreckage_path = /obj/structure/mech_wreckage
// Access updating/container.
var/obj/item/card/id/mecha/access_card
// Mob we're currently paired with or following | the names are saved to prevent metagaming when returning diagnostics
var/datum/weakref/leader
var/leader_name
var/datum/weakref/following
var/following_name
// Orders from our leader
var/nickname // we'll respond to our name or our nickname
var/follow_distance = 3
// Mob currently piloting the mech.
var/list/pilots
var/list/pilot_overlays
// Remote control stuff
var/remote = FALSE // Spawns a robotic pilot to be remote controlled
var/remote_type = /obj/item/remote_mecha
var/does_hardpoint_lock = TRUE
var/mob/living/simple_animal/spiderbot/dummy // The remote controlled dummy
var/dummy_type = /mob/living/simple_animal/spiderbot
var/dummy_colour
// Visible external components. Not strictly accurately named for non-humanoid machines (submarines) but w/e
var/obj/item/mech_component/manipulators/arms
var/obj/item/mech_component/propulsion/legs
var/obj/item/mech_component/sensors/head
var/obj/item/mech_component/chassis/body
// Equipment tracking vars.
var/obj/item/mecha_equipment/selected_system
var/selected_hardpoint
var/list/hardpoints = list()
var/hardpoints_locked
var/maintenance_protocols
var/lockdown
var/entry_speed = 30
var/loudening = FALSE // whether we're increasing the speech volume of our pilot
// Material
var/material/material
// Cockpit access vars.
var/hatch_closed = FALSE
var/hatch_locked = FALSE
var/force_locked = FALSE // Is it possible to unlock the hatch?
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
var/atom/movable/screen/mecha/toggle/hatch_open/hud_open
var/atom/movable/screen/mecha/power/hud_power
var/atom/movable/screen/mecha/toggle/power_control/hud_power_control
//POWER
var/power = MECH_POWER_OFF
/mob/living/heavy_vehicle/Destroy()
unassign_leader()
unassign_following()
selected_system = null
for(var/hardpoint in hardpoints)
var/obj/item/S = remove_system(hardpoint, force = 1)
if(S)
QDEL_NULL(S)
hardpoints = null
for(var/thing in pilots)
var/mob/pilot = thing
if(pilot.client)
pilot.client.screen -= hud_elements
pilot.client.images -= hud_elements
pilot.forceMove(get_turf(src))
pilots = null
QDEL_LIST(hud_elements)
if(remote_network)
SSvirtualreality.remove_mech(src, remote_network)
hardpoint_hud_elements = null
QDEL_NULL(access_card)
QDEL_NULL(arms)
QDEL_NULL(legs)
QDEL_NULL(head)
QDEL_NULL(body)
QDEL_NULL(hud_health)
QDEL_NULL(hud_open)
QDEL_NULL(hud_power)
QDEL_NULL(hud_power_control)
QDEL_NULL(camera)
QDEL_NULL(radio)
. = ..()
/mob/living/heavy_vehicle/IsAdvancedToolUser()
return TRUE
/mob/living/heavy_vehicle/get_examine_text(mob/user, distance, is_adjacent, infix, suffix)
SHOULD_CALL_PARENT(FALSE) //Special snowflake case
. = list()
if(!user || !user.client)
return TRUE
. += "That's \a <b>[src]</b>."
if(desc)
to_chat(user, desc)
if(LAZYLEN(pilots) && (!hatch_closed || body.pilot_coverage < 100 || body.transparent_cabin))
if(length(pilots) == 0)
. += "It has <b>no pilot</b>."
else
for(var/pilot in pilots)
if(ismob(pilot))
var/mob/M = pilot
. += "It is being <b>piloted</b> by <a href='byond://?src=[REF(src)];examine=[REF(M)]'>[M.name]</a>."
else
. += "It is being <b>piloted</b> by <b>[pilot]</b>."
if(hardpoints.len)
. += SPAN_NOTICE("It has the following hardpoints:")
for(var/hardpoint in hardpoints)
var/obj/item/I = hardpoints[hardpoint]
. += "- <b>[hardpoint]</b>: [istype(I) ? "<a href='byond://?src=[REF(src)];examine=[REF(I)]'>[I.name]</a>" : "nothing"]."
else
. += "It has <b>no visible hardpoints</b>."
for(var/obj/item/mech_component/thing in list(arms, legs, head, body))
if(!thing)
continue
var/damage_string = ""
switch(thing.damage_state)
if(1)
damage_string = "undamaged"
if(2)
damage_string = SPAN_WARNING("damaged")
if(3)
damage_string = SPAN_WARNING("badly damaged")
if(4)
damage_string = SPAN_DANGER("destroyed")
. += "Its <b>[thing.name]</b> [thing.gender == PLURAL ? "are" : "is"] [damage_string]."
/mob/living/heavy_vehicle/Topic(href,href_list[])
if (href_list["examine"])
var/mob/M = locate(href_list["examine"])
if(!M)
return
examinate(usr, M)
/mob/living/heavy_vehicle/Initialize(mapload, var/obj/structure/heavy_vehicle_frame/source_frame)
..()
if(!access_card) access_card = new (src)
if(offset_x) pixel_x = offset_x
if(offset_y) pixel_y = offset_y
radio = new(src)
// Grab all the supplied components.
if(source_frame)
if(source_frame.set_name)
name = source_frame.set_name
if(source_frame.arms)
source_frame.arms.forceMove(src)
arms = source_frame.arms
if(source_frame.legs)
source_frame.legs.forceMove(src)
legs = source_frame.legs
if(legs.hover) //Checks here if hoverthrusters
pass_flags |= PASSRAILING
if(source_frame.head)
source_frame.head.forceMove(src)
head = source_frame.head
if(source_frame.body)
source_frame.body.forceMove(src)
body = source_frame.body
if(body.cell)
RegisterSignal(body.cell, COMSIG_CELL_CHARGE, PROC_REF(handle_cell_charge))
updatehealth()
// Generate hardpoint list.
for(var/obj/item/mech_component/thing in list(arms, legs, head, body))
if(thing && thing.has_hardpoints.len)
for(var/hardpoint in thing.has_hardpoints)
hardpoints[hardpoint] = null
if(head && head.radio)
radio = new(src)
if(!camera)
camera = new /obj/machinery/camera(src, 0, TRUE, TRUE)
camera.c_tag = name
camera.replace_networks(list(NETWORK_MECHS))
// Create HUD.
instantiate_hud()
// Build icon.
update_icon()
add_language(LANGUAGE_TCB)
default_language = GLOB.all_languages[LANGUAGE_TCB]
. = INITIALIZE_HINT_LATELOAD
/mob/living/heavy_vehicle/LateInitialize()
var/obj/machinery/mech_recharger/MR = locate() in get_turf(src)
if(MR)
MR.start_charging(src)
/mob/living/heavy_vehicle/return_air()
return (body && body.pilot_coverage >= 100 && hatch_closed) ? body.cockpit : loc?.return_air()
/mob/living/heavy_vehicle/GetIdCard()
return access_card
/// Checks if mech can be powered on/off, sends message to pilot if failed
/// `var/remote` can be set to TRUE to have proc adjust where messages and hud elements are presented
/// If `remote` is TRUE, messages and other hud elements are called on the exosuit itself to prevent wierdness, and errors are handled in `handle_hear_say()`
/mob/living/heavy_vehicle/proc/toggle_power(var/mob/user, var/remote = FALSE)
// if remotely called, send these messages to the exosuit, not the person calling this proc
var/reciever = user
if(remote)
reciever = src
if(power == MECH_POWER_TRANSITION)
to_chat(reciever, SPAN_NOTICE("Power transition in progress. Please wait."))
else if(power == MECH_POWER_ON) //Turning it off is instant
playsound(src, 'sound/mecha/mech-shutdown.ogg', 100, 0)
power = MECH_POWER_OFF
else if(get_cell(TRUE))
//Start power up sequence
power = MECH_POWER_TRANSITION
playsound(src, 'sound/mecha/powerup.ogg', 50, 0)
if(do_after(reciever, 1.5 SECONDS) && power == MECH_POWER_TRANSITION)
playsound(src, 'sound/mecha/nominal.ogg', 50, 0)
power = MECH_POWER_ON
else
to_chat(reciever, SPAN_WARNING("You abort the powerup sequence."))
power = MECH_POWER_OFF
hud_power_control?.queue_icon_update()
else
to_chat(reciever, SPAN_WARNING("Error: No power cell was detected."))
/obj/item/radio/exosuit
name = "exosuit radio"
cell = null
/obj/item/radio/exosuit/get_cell()
. = ..()
if(!.)
var/mob/living/heavy_vehicle/E = loc
if(istype(E))
return E.get_cell()
/obj/item/radio/exosuit/ui_host()
var/mob/living/heavy_vehicle/E = loc
if(istype(E))
return E
return null
/obj/item/radio/exosuit/attack_self(var/mob/user)
var/mob/living/heavy_vehicle/exosuit = loc
if(istype(exosuit) && exosuit.head && exosuit.head.radio && exosuit.head.radio.is_functional())
user.set_machine(src)
interact(user)
else
to_chat(user, SPAN_WARNING("The radio is too damaged to function."))
/obj/item/radio/exosuit/CanUseTopic()
. = ..()
if(.)
var/mob/living/heavy_vehicle/exosuit = loc
if(istype(exosuit) && exosuit.head && exosuit.head.radio && exosuit.head.radio.is_functional())
return ..()
/obj/item/radio/exosuit/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/ui_state/state = GLOB.mech_state)
. = ..()
/mob/living/heavy_vehicle/proc/become_remote()
for(var/mob/user in pilots)
eject(user, FALSE)
remote = TRUE
name = name + " \"[pick("Jaeger", "Reaver", "Templar", "Juggernaut", "Basilisk")]-[rand(0, 999)]\""
if(!remote_network)
remote_network = REMOTE_GENERIC_MECH
SSvirtualreality.add_mech(src, remote_network)
if(hatch_closed)
hatch_closed = FALSE
dummy = new dummy_type(get_turf(src))
dummy.real_name = "Remote-Bot"
dummy.name = dummy.real_name
// Give dummy a blank encryption key for later editing if spiderbot
if(istype(dummy, /mob/living/simple_animal/spiderbot) && !istype(dummy, /mob/living/simple_animal/spiderbot/ai))
dummy.radio.keyslot = new /obj/item/encryptionkey
remove_verb(dummy, /mob/living/proc/ventcrawl)
remove_verb(dummy, /mob/living/proc/hide)
if(dummy_colour)
dummy.color = dummy_colour
enter(dummy, TRUE)
if(!hatch_closed)
hatch_closed = TRUE
hatch_locked = TRUE
if(does_hardpoint_lock)
hardpoints_locked = TRUE
force_locked = TRUE
update_icon()
/mob/living/heavy_vehicle/is_anti_materiel_vulnerable()
return TRUE