More Mech Damage Feedback (#22118)

This PR adds some new "Damage Feedback" interactions to mechs, which I'm
adding in response to what I think is player perception of mechs being
invincible. Previously mech manipulators and torsos had no actual
mechanics for damage states, and having 0 damage on a mechs arms was
functionally the same thing as having 100 damage, so long as the arm
wasn't completely destroyed.

This PR changes that by making it so that mech manipulators have a
chance to fail (loudly and visibly) on use with a probability that
varies based on how much damage the arms have taken. Additionally,
there's also now a mechanic for mech ejections and hatch opening to fail
if the torso has taken sufficient damage, with chances that also vary
based on how much damage said torso has taken. In both cases, the mech
plays audible and visual effects to indicate to the players involved
that it is being hindered by damage.


https://github.com/user-attachments/assets/934e4561-1fda-4c47-9770-438960834aa9

Yes this does mean that you can smash someone's hatch closed, to a point
you'll need a welder to get them out.

<img width="772" height="181" alt="image"
src="https://github.com/user-attachments/assets/75f98060-7c4c-468b-bd31-e6c4ec4c0be2"
/>
This commit is contained in:
VMSolidus
2026-04-04 22:27:01 +00:00
committed by GitHub
parent d313f70906
commit a436fa65a6
12 changed files with 118 additions and 37 deletions
@@ -9,13 +9,19 @@
var/total_damage = 0
var/brute_damage = 0
var/burn_damage = 0
/// The maximum combined brute and burn damage that the mech component can take before becoming completely broken.
var/max_damage = 120
var/damage_state = 1
var/list/has_hardpoints = list()
var/power_use = 0
matter = list(DEFAULT_WALL_MATERIAL = 15000, MATERIAL_PLASTIC = 1000, MATERIAL_OSMIUM = 500)
dir = SOUTH
/// Half of the baseline chance that attempting to use these arms will create sparks. Actual chance is twice this since the motivator damage contributes to the ratio.
var/spark_chance_ratio = 50
/obj/item/mech_component/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
if(ready_to_install())
@@ -32,6 +32,13 @@
var/hide_pilot = TRUE
has_hardpoints = list(HARDPOINT_BACK, HARDPOINT_LEFT_SHOULDER, HARDPOINT_RIGHT_SHOULDER)
/**
* Maximum % chance for an ejection to fail based on how much damage the mech's torso has taken.
* If you're stuck, better hope there's a friend available to bail you out..
* Or you can keep the hatch open if you would rather risk getting zipped by incoming fire.
*/
var/ejection_fail_chance = 75
/obj/item/mech_component/chassis/Initialize()
. = ..()
AddComponent(/datum/component/armor, armor, ARMOR_TYPE_STANDARD|ARMOR_TYPE_EXOSUIT)
@@ -211,6 +211,8 @@
/atom/movable/screen/mecha/toggle/air/toggled()
toggled = !toggled
owner.use_air = toggled
if (owner.use_air)
playsound(owner.loc, 'sound/effects/internals.ogg', 75, 1)
var/main_color = owner.use_air ? "#d1d1d1" : "#525252"
maptext = "<span style=\"font-family: 'Small Fonts'; color: [main_color]; -dm-text-outline: 1 #242424; font-size: 6px;\">AIR</span>"
notify_user(usr, SPAN_NOTICE("Auxiliary atmospheric system [owner.use_air ? "enabled" : "disabled"]."))
@@ -286,6 +288,7 @@
maptext_y = 11
maptext_x = 5
notify_user(usr, SPAN_NOTICE("The [owner.body.hatch_descriptor] is [owner.hatch_locked ? "now" : "no longer" ] locked."))
playsound(owner.loc, 'sound/effects/magnetclamp.ogg', 100, 1)
/atom/movable/screen/mecha/toggle/hatch_open
name = "open or close hatch"
@@ -298,15 +301,29 @@
maptext_x = owner.hatch_closed ? 4 : 3
/atom/movable/screen/mecha/toggle/hatch_open/toggled(var/notify_user = TRUE)
var/obj/item/mech_component/chassis/body = owner.body
if(!body) // Edge case for the mech SOMEHOW not having a body. Players shouldn't ever see this. A mech without a body should be in the process of exploding.
notify_user(usr, SPAN_WARNING("What hatch? There's nothing left to open. If you're seeing this, please submit a bug report to our github issue tracker and tell us how!"))
return FALSE
if(owner.hatch_locked && owner.hatch_closed)
notify_user(usr, SPAN_WARNING("You cannot open the hatch while it is locked."))
return
return FALSE
if(owner.hatch_closed && body.total_damage > 1 && prob((body.total_damage / body.max_damage) * body.ejection_fail_chance))
to_chat(usr, SPAN_WARNING("You pull the hatch release. The hatch bolts hiss and clang with a deafening screech, but the hatch fails to open!"))
spark(owner, 6, GLOB.alldirs)
playsound(owner.loc, 'sound/effects/metalhit.ogg', 75, 1)
return FALSE
toggled = !toggled
owner.hatch_closed = toggled
playsound(owner.loc, owner.hatch_closed ? 'sound/effects/metal_close.ogg' : 'sound/effects/air_seal.ogg', 75, 1)
if(notify_user)
notify_user(usr, SPAN_NOTICE("The [owner.body.hatch_descriptor] is now [owner.hatch_closed ? "closed" : "open" ]."))
update_icon()
owner.update_icon()
return TRUE
// This is basically just a holder for the updates the mech does.
/atom/movable/screen/mecha/health
+70 -28
View File
@@ -43,24 +43,29 @@
setClickCooldown(5)
return
if(!(user in pilots) && user != src)
return
// Are we facing the target?
if(!(get_dir(src, A) & dir))
return
if(!canClick())
if(/* Do we have a pilot? */\
!(user in pilots) && user != src \
/* Are we facing the target? */\
|| !(get_dir(src, A) & dir) \
/* Can we click on them? */\
|| !canClick())
return
// Check if the arms were completely destroyed.
if(!arms)
to_chat(user, SPAN_WARNING("\The [src] has no manipulators!"))
setClickCooldown(3)
// Give some visible feedback for the mech damage preventing this.
spark(src, 2, GLOB.alldirs)
return
if(!arms.motivator || !arms.motivator.is_functional())
var/obj/item/robot_parts/robot_component/actuator/motivator = arms.motivator
// Check if the motivators were broken.
if(!motivator || !motivator.is_functional())
to_chat(user, SPAN_WARNING("Your motivators are damaged! You can't use your manipulators!"))
setClickCooldown(15)
// Give some visible feedback for the mech damage preventing this.
spark(src, 2, GLOB.alldirs)
return
if(!checked_use_cell(arms.power_use * CELLRATE))
@@ -74,10 +79,18 @@
else
zone_sel.set_selected_zone("chest", user)
// Get the ratio of damage over max damage
var/spark_chance = (motivator.total_dam / motivator.max_dam + arms.total_damage / arms.max_damage) \
/* Multiply by the spark chance */\
* arms.spark_chance_ratio \
/* Then add the contribution from EMP damage. */\
+ emp_damage
// You may attack the target with your exosuit FIST if you're malfunctioning.
var/failed = FALSE
if(emp_damage > EMP_ATTACK_DISRUPT && prob(emp_damage*2))
to_chat(user, SPAN_WARNING("The wiring sparks as you attempt to control the exosuit!"))
if(prob(spark_chance))
to_chat(user, SPAN_WARNING("Your exosuit's manipulators spark as you attempt to control them!"))
spark(src, 3, GLOB.alldirs)
failed = TRUE
if(!failed)
@@ -146,7 +159,7 @@
D.TryToSwitchState(user)
return
else if(adj)
setClickCooldown(arms ? arms.action_delay : 15)
setClickCooldown(arms.action_delay)
playsound(src.loc, arms.punch_sound, 45 + 25 * (arms.melee_damage / 50), -1 )
if(ismob(A))
var/mob/target = A
@@ -246,7 +259,8 @@
if(hatch_locked)
if(!silent) to_chat(user, SPAN_WARNING("The [body.hatch_descriptor] is locked."))
return
hud_open.toggled(FALSE)
if(!hud_open.toggled(FALSE))
return
if(!silent)
to_chat(user, SPAN_NOTICE("You open the hatch and climb out of \the [src]."))
else
@@ -362,7 +376,7 @@
/mob/living/heavy_vehicle/Move(atom/newloc, direct, glide_size_override = 0, update_dir = TRUE)
// They shouldn't get to this proc without legs in the first place, but its okay to guard here.
if (!legs)
if (!legs || !legs.motivator)
return
. = ..()
set_glide_size(DELAY_TO_GLIDE_SIZE(next_mecha_move - world.time))
@@ -372,6 +386,21 @@
use_cell_power(legs.power_use * CELLRATE)
update_icon()
// Handling for leg damage feedback on movement.
var/obj/item/robot_parts/robot_component/actuator/motivator = legs.motivator
// Get the ratio of damage over max damage.
var/spark_chance = (motivator.total_dam / motivator.max_dam + legs.total_damage / legs.max_damage) \
/* Then multiply by the spark chance. */\
* legs.spark_chance_ratio \
/* And finally add the contribution from EMP damage. */\
+ emp_damage
if(prob(spark_chance))
for (var/mob/pilot in pilots)
to_chat(pilot, SPAN_WARNING("Your exosuit's legs spark as you attempt to control them!"))
spark(src, 3, GLOB.alldirs)
/mob/living/heavy_vehicle/Post_Incorpmove()
if(istype(hardpoints[HARDPOINT_BACK], /obj/item/mecha_equipment/phazon))
var/obj/item/mecha_equipment/phazon/PZ = hardpoints[HARDPOINT_BACK]
@@ -537,28 +566,41 @@
return ..()
/mob/living/heavy_vehicle/attack_hand(var/mob/user)
// Drag the pilot out if possible.
if(user.a_intent == I_GRAB)
if(!LAZYLEN(pilots))
to_chat(user, SPAN_WARNING("There is nobody inside \the [src]."))
else if(!hatch_closed)
// Try to attack on harm intent.
if(user.a_intent == I_HURT)
attack_generic(user)
return
// Otherwise try to toggle the hatch.
if(!body) // Edge case for the mech SOMEHOW not having a body. Players shouldn't ever see this. A mech without a body should be in the process of exploding.
to_chat(user, SPAN_WARNING("What hatch? There's nothing left to open. If you're seeing this, please submit a bug report to our github issue tracker and tell us how!"))
return
if(hatch_locked)
to_chat(user, SPAN_WARNING("The [body.hatch_descriptor] is locked."))
return
if(!hatch_closed)
// Drag the pilot out if possible.
if(user.a_intent == I_GRAB)
if(!LAZYLEN(pilots))
to_chat(user, SPAN_WARNING("There is nobody inside \the [src]."))
return
var/mob/pilot = pick(pilots)
user.visible_message(SPAN_DANGER("\The [user] is trying to pull \the [pilot] out of \the [src]!"))
if(do_after(user, 30) && user.Adjacent(src) && (pilot in pilots) && !hatch_closed)
user.visible_message(SPAN_DANGER("\The [user] drags \the [pilot] out of \the [src]!"))
eject(pilot, silent=1)
return
else if (body.total_damage > 1 && prob((body.total_damage / body.max_damage) * body.ejection_fail_chance))
to_chat(user, SPAN_WARNING("You pull the hatch release. The hatch bolts hiss and clang with a deafening screech, but the hatch fails to open!"))
spark(src, 6, GLOB.alldirs)
playsound(loc, 'sound/effects/metalhit.ogg', 75, 1)
return
if(user.a_intent == I_HURT)
attack_generic(user)
return
// Otherwise toggle the hatch.
if(hatch_locked)
to_chat(user, SPAN_WARNING("The [body.hatch_descriptor] is locked."))
return
hatch_closed = !hatch_closed
playsound(loc, hatch_closed ? 'sound/effects/metal_close.ogg' : 'sound/effects/air_seal.ogg', 75, 1)
to_chat(user, SPAN_NOTICE("You [hatch_closed ? "close" : "open"] the [body.hatch_descriptor]."))
hud_open.update_icon()
update_icon()
+1 -1
View File
@@ -19,7 +19,7 @@
icon_state = "combat_arms"
melee_damage = 30
action_delay = 5
max_damage = 130
max_damage = 195
power_use = 2500
has_hardpoints = list(HARDPOINT_LEFT_SHOULDER, HARDPOINT_RIGHT_SHOULDER)
+1 -1
View File
@@ -24,7 +24,7 @@
icon_state = "cult_arms"
melee_damage = 50
action_delay = 5
max_damage = 90
max_damage = 135
power_use = 3500
punch_sound = 'sound/mecha/mech_punch_slow.ogg'
+1 -1
View File
@@ -20,7 +20,7 @@
desc = "Designed to function where any other piece of equipment would have long fallen apart, the Hephaestus Superheavy Lifter series can take a beating and excel at delivering it."
melee_damage = 50
action_delay = 15
max_damage = 200
max_damage = 300
power_use = 3500
punch_sound = 'sound/mecha/mech_punch_slow.ogg'
+1 -1
View File
@@ -18,7 +18,7 @@
icon_state = "light_arms"
melee_damage = 15
action_delay = 15
max_damage = 100
max_damage = 150
power_use = 1500
desc = "As flexible as they are fragile, these Bishop Cybernetics manipulators can follow a pilot's movements in close to real time."
punch_sound = 'sound/mecha/mech_punch_fast.ogg'
@@ -5,7 +5,7 @@
desc = "These basic, heavy plasteel mounts are used for mounting only the meanest weapons. Probably good at breaking things, too."
melee_damage = 50
action_delay = 15
max_damage = 4000
max_damage = 900
power_use = 3500
has_hardpoints = list(HARDPOINT_LEFT_SHOULDER, HARDPOINT_RIGHT_SHOULDER)
punch_sound = 'sound/mecha/mech_punch_slow.ogg'
@@ -42,7 +42,7 @@
/obj/item/mech_component/manipulators/ripley
name = "exosuit arms"
exosuit_desc_string = "heavy-duty industrial lifters"
max_damage = 150
max_damage = 225
power_use = 1000
melee_damage = 40
desc = "The Xion Manufacturing Group Digital Interaction Manifolds allow you poke untold dangers from the relative safety of your cockpit."
+2 -2
View File
@@ -18,7 +18,7 @@
icon_state = "egg_arms"
melee_damage = 15
action_delay = 5
max_damage = 100
max_damage = 150
power_use = 2500
has_hardpoints = list(HARDPOINT_RIGHT_HAND, HARDPOINT_LEFT_HAND)
@@ -81,7 +81,7 @@
desc = "Armored robotics arms designed to carry large weapons."
icon_state = "strong_egg_arms"
melee_damage = 20
max_damage = 120
max_damage = 180
/obj/item/mech_component/sensors/pra_egg/armored
name = "armored P'kus-3 sensors"