From d47cdb8f044ae19405fb3cc395e065a94ad1e7fb Mon Sep 17 00:00:00 2001 From: FenodyreeAv Date: Thu, 6 Aug 2026 10:40:33 +0100 Subject: [PATCH] Fixes many mech bugs (#22983) Four major fixes. These are all pretty clearly bugs, but the result is a substantial mech nerf (except EMPs against EM armor.) First, EMP armour. EMP armour did nothing, it was partially ported to use TG's Element flag system, this finishes that. Rather than make the EMP armour immune to EMP, it spends power to negate it. The cost is 1000 / severity. Heavy EMP is 1, Light EMP is 2. This is 5% of a basic mech cell for light, 10% for heavy. Also fixes the pilot getting EMP'd twice for every EMP act on the mech. Second, open hatches let attacks hit the pilot. Chance is 80% from the front, 20% from the sides. The chance is mostly so that turrets that always aim torso don't shoot mechs forever, always hitting the pilot inside. Third, cockpit damage actually matters now. Previously shooting the mech in the chest was nearly worthless, no important components were there and destroying it did not destroy the mech. Now if the cockpit is completely destroyed, attacks against it penetrate through to hit the pilot. These attacks still account for the mech's armour. Combined with the hatch getting stuck closed, you can have tense rescues of pilots bleeding out inside their suits. Fourth, the mech running out of power actually stops it doing things. Uranium and Phoron power cores allowed mechs to keep running forever, even at fractional power, because they recharged slightly every tick. Now if the mech ever cannot afford the power cost of an action it shuts down. It can be powered back up immediately if the power core recharges it, but it cannot now do costly actions at single digit power. I fixed mech combat shields. They suck, they use the auras system which does not play nicely with the new projectile code. Recursive calls and spaghetti mess. Works for now but could use a complete rewrite. --------- Signed-off-by: FenodyreeAv --- code/game/objects/auras/auras.dm | 9 ++ .../modules/heavy_vehicle/components/armor.dm | 6 +- code/modules/heavy_vehicle/components/body.dm | 2 +- .../modules/heavy_vehicle/equipment/combat.dm | 68 ++++++++++----- code/modules/heavy_vehicle/mech_damage.dm | 82 ++++++++++++++----- code/modules/heavy_vehicle/mech_helpers.dm | 16 +++- code/modules/heavy_vehicle/mecha.dm | 14 +++- code/modules/mob/living/living_defense.dm | 3 +- html/changelogs/Fenodyree-MechBugfixes.yml | 10 +++ 9 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 html/changelogs/Fenodyree-MechBugfixes.yml diff --git a/code/game/objects/auras/auras.dm b/code/game/objects/auras/auras.dm index 2085b5e12af..6a4c49698a7 100644 --- a/code/game/objects/auras/auras.dm +++ b/code/game/objects/auras/auras.dm @@ -15,6 +15,15 @@ They should also be used for when you want to effect the ENTIRE mob, like having return COMPONENT_BULLET_BLOCKED +/obj/aura/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) + var/result = ..() + if(result == BULLET_ACT_BLOCK) //The bullet was blocked, don't check the remaining auras. + return AURA_FALSE|AURA_CANCEL + else if(result == BULLET_ACT_FORCE_PIERCE) //The bullet was forced to pierce, check the remaining auras to see if they block. + return AURA_FALSE + else + return + /obj/aura/Destroy() if(user) user.remove_aura(src) diff --git a/code/modules/heavy_vehicle/components/armor.dm b/code/modules/heavy_vehicle/components/armor.dm index 8f1a64c3256..91e5a27b094 100644 --- a/code/modules/heavy_vehicle/components/armor.dm +++ b/code/modules/heavy_vehicle/components/armor.dm @@ -11,6 +11,7 @@ RAD = ARMOR_RAD_MINOR ) origin_tech = list(TECH_MATERIAL = 1) + var/emp_protection = 0 /obj/item/robot_parts/robot_component/armor/mech/Initialize() . = ..() @@ -33,8 +34,8 @@ origin_tech = list(TECH_MATERIAL = 3) /obj/item/robot_parts/robot_component/armor/mech/em - name = "EM-shielded armor plating" - desc = "A shielded plating that sorrounds the eletronics and protects them from electromagnetic radiation." + name = "anti EM reactive armor plating" + desc = "An advanced active-defense system that insulates the eletronics and destructively interferes with electromagnetic radiation." icon_state = "armor_e" icon_state_broken = "armor_e_broken" armor = list( @@ -47,6 +48,7 @@ RAD = ARMOR_RAD_SMALL ) origin_tech = list(TECH_MATERIAL = 3) + emp_protection = 1 /obj/item/robot_parts/robot_component/armor/mech/combat name = "heavy combat plating" diff --git a/code/modules/heavy_vehicle/components/body.dm b/code/modules/heavy_vehicle/components/body.dm index 38ec7742978..ec8d2ea0430 100644 --- a/code/modules/heavy_vehicle/components/body.dm +++ b/code/modules/heavy_vehicle/components/body.dm @@ -19,7 +19,7 @@ var/obj/item/robot_parts/robot_component/diagnosis_unit/diagnostics var/obj/item/cell/mecha/cell var/cell_type = /obj/item/cell/mecha - var/obj/item/robot_parts/robot_component/armor/mech_armor + var/obj/item/robot_parts/robot_component/armor/mech/mech_armor var/obj/structure/machinery/portable_atmospherics/canister/air_supply var/datum/gas_mixture/cockpit var/pilot_offset_x = 0 diff --git a/code/modules/heavy_vehicle/equipment/combat.dm b/code/modules/heavy_vehicle/equipment/combat.dm index 931219f1c8c..09376c49205 100644 --- a/code/modules/heavy_vehicle/equipment/combat.dm +++ b/code/modules/heavy_vehicle/equipment/combat.dm @@ -296,11 +296,12 @@ desc = "The Hephaestus Armature system is a well liked energy deflector system designed to stop any projectile before it has a chance to become a threat." icon_state = "shield_droid" var/obj/aura/mechshield/aura - var/max_charge = 150 - var/charge = 150 + var/max_charge = 100 + var/charge = 100 var/last_recharge = 0 var/charging_rate = 7500 * CELLRATE var/cooldown = 3.5 SECONDS // Time until we can recharge again after a blocked impact + var/efficiency = 0.1 // How much power is required to recharge 1 point of shield charge restricted_hardpoints = list(HARDPOINT_BACK) restricted_software = list(MECH_SOFTWARE_WEAPONS) module_hints = list( @@ -331,7 +332,7 @@ last_recharge = world.time - if(difference > 0) + if(difference >= 0) for(var/mob/pilot in owner.pilots) to_chat(pilot, FONT_LARGE(SPAN_WARNING("Warning: Deflector shield failure detected, shutting down."))) toggle() @@ -379,8 +380,10 @@ if((world.time - last_recharge) < cooldown) return - var/actual_required_power = clamp(max_charge - charge, 0, charging_rate) - owner.use_cell_power(actual_required_power) + var/actual_required_power = clamp(max_charge * efficiency - charge * efficiency, 0, charging_rate) + var/actual_power_used = owner.use_cell_power(actual_required_power) + if(actual_power_used > 0) + charge = clamp(charge + actual_power_used, 0, max_charge) /obj/item/mecha_equipment/shield/get_hardpoint_status_value() return charge / max_charge @@ -434,24 +437,51 @@ else icon_state = "shield_null" +/obj/aura/mechshield/handle_bullet_act(datum/source, obj/projectile/projectile) + if(!active || !shields || !shields.charge) + return FALSE + + var/incoming_attack_direction = projectile.starting ? get_dir(shields, projectile.starting) : null + if(!(dir & incoming_attack_direction)) + return FALSE + + var/damage = projectile.get_structure_damage() + if(!damage) + damage = projectile.damage + + projectile.damage = shields.stop_damage(damage) + user.visible_message(SPAN_WARNING("\The [shields.owner]'s shields flash and crackle.")) + flick("shield_impact", src) + playsound(user, 'sound/effects/basscannon.ogg', 35, TRUE) + //light up the night. + new /obj/effect/smoke/illumination(get_turf(src), 5, 4, 1, "#ffffff") + spark(get_turf(src), 5, GLOB.alldirs) + playsound(get_turf(src), SFX_SPARKS, 25, TRUE) + return COMPONENT_BULLET_BLOCKED + /obj/aura/mechshield/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) - if(!active) - return + SHOULD_CALL_PARENT(FALSE) - . = ..() + if(!active || !shields || !shields.charge) + return FALSE - if(shields?.charge) - hitting_projectile.damage = shields.stop_damage(hitting_projectile.damage) - user.visible_message(SPAN_WARNING("\The [shields.owner]'s shields flash and crackle.")) - flick("shield_impact", src) - playsound(user, 'sound/effects/basscannon.ogg', 35, TRUE) - //light up the night. - new /obj/effect/smoke/illumination(get_turf(src), 5, 4, 1, "#ffffff") - if(hitting_projectile.damage <= 0) - return AURA_FALSE|AURA_CANCEL + var/incoming_attack_direction = hitting_projectile.starting ? get_dir(shields, hitting_projectile.starting) : null + if(!(dir & incoming_attack_direction)) + return FALSE - spark(get_turf(src), 5, GLOB.alldirs) - playsound(get_turf(src), SFX_SPARKS, 25, TRUE) + var/damage = hitting_projectile.get_structure_damage() + if(!damage) + damage = hitting_projectile.damage + + hitting_projectile.damage = shields.stop_damage(damage) + user.visible_message(SPAN_WARNING("\The [shields.owner]'s shields flash and crackle.")) + flick("shield_impact", src) + playsound(user, 'sound/effects/basscannon.ogg', 35, TRUE) + //light up the night. + new /obj/effect/smoke/illumination(get_turf(src), 5, 4, 1, "#ffffff") + spark(get_turf(src), 5, GLOB.alldirs) + playsound(get_turf(src), SFX_SPARKS, 25, TRUE) + return AURA_FALSE|AURA_CANCEL /obj/aura/mechshield/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) . = ..() diff --git a/code/modules/heavy_vehicle/mech_damage.dm b/code/modules/heavy_vehicle/mech_damage.dm index ae0bec5b19b..22cdc53ceb6 100644 --- a/code/modules/heavy_vehicle/mech_damage.dm +++ b/code/modules/heavy_vehicle/mech_damage.dm @@ -1,8 +1,35 @@ +///Returns TRUE if an incoming attack should target the pilot instead of the mech, FALSE otherwise. +/mob/living/heavy_vehicle/proc/should_target_pilot(atom/attack_source = null) + if(!body || !LAZYLEN(pilots)) //If there is no pilot, or the mech somehow doesn't have a body don't try hit the pilot. + return FALSE + + var/incoming_attack_direction = attack_source ? get_dir(src, attack_source) : null + + if (istype(hardpoints[HARDPOINT_BACK], /obj/item/mecha_equipment/shield)) //If the mech has a shield. + var/obj/item/mecha_equipment/shield/shield = hardpoints[HARDPOINT_BACK] + if (shield.active && shield.aura && shield.aura.active) //If the shield is active. + if (incoming_attack_direction && (dir & incoming_attack_direction)) //If the attack comes from the front and the shield is active, the pilot is protected. + return FALSE + + if(!prob(body.pilot_coverage)) //If the cockpit doesn't cover the pilot completely, attacks have a chance to hit the pilot instead of the mech. + return TRUE + + if(!hatch_closed) //Open hatches only expose the pilot to attacks from the front and a little bit from the sides. + if(incoming_attack_direction) + if (dir & incoming_attack_direction) + if(prob(80)) //80% chance to hit the pilot from the front if the hatch is open. If this is 100% turrets will shoot mechs with open hatches forever. + return TRUE + if ((turn(dir, 90) & incoming_attack_direction) || (turn(dir, -90) & incoming_attack_direction)) + if(prob(20)) //20% chance to hit the pilot from the sides if the hatch is open. + return TRUE + + return FALSE + /mob/living/heavy_vehicle/apply_effect(var/effect = 0,var/effecttype = STUN, var/blocked = 0) if(!effect || (blocked >= 100)) return 0 - if(LAZYLEN(pilots) && (!hatch_closed || !prob(body.pilot_coverage))) + if(should_target_pilot()) if(effect > 0 && effecttype == DAMAGE_RADIATION) var/mob/living/pilot = pick(pilots) return pilot.apply_effect(effect, effecttype, blocked) @@ -10,11 +37,18 @@ . = ..() /mob/living/heavy_vehicle/hitby(atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) - if(LAZYLEN(pilots) && (!hatch_closed || !prob(body.pilot_coverage))) + if(should_target_pilot(hitting_atom)) var/mob/living/pilot = pick(pilots) return pilot.hitby(arglist(args)) . = ..() +/mob/living/heavy_vehicle/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) + if(should_target_pilot(hitting_projectile?.starting)) + var/mob/living/pilot = pick(pilots) + if(pilot) + return pilot.bullet_act(hitting_projectile, def_zone, piercing_hit) + return ..() + /mob/living/heavy_vehicle/get_armors_by_zone(def_zone, damage_type, damage_flags) . = ..() if(body) @@ -82,6 +116,13 @@ return 0 var/target = zoneToComponent(def_zone) + + if(target == body && body.damage_state == MECH_COMPONENT_DAMAGE_DAMAGED_TOTAL) //If the cockpit is destroyed, subsequent damage is applied to the pilot, modified by the mech's armour. + if(body && LAZYLEN(pilots)) + var/mob/living/pilot = pick(pilots) + visible_message(SPAN_DANGER("\The [used_weapon] pierces the mangled cockpit of \the [src], striking the pilot inside!")) + pilot.apply_damage(damage, damagetype, def_zone, used_weapon, damage_flags, armor_pen, silent = FALSE) + //Only 2 types of damage concern mechs and vehicles switch(damagetype) if(DAMAGE_BRUTE) @@ -114,25 +155,26 @@ /mob/living/heavy_vehicle/emp_act(severity) . = ..() + if(. & EMP_PROTECT_SELF) + var/obj/item/cell/C = get_cell() + if(C.charge > 1000) //Don't completely run a mech out of power this way. + var/power_used = use_cell_power(1000 / severity) + var/percent_power_used = 0 + if(C && C.maxcharge) + percent_power_used = round((power_used / C.maxcharge) * 100) + visible_message(SPAN_NOTICE("\The [src]'s active EM defenses flash brightly, negating the EMP!")) + for(var/pilot in pilots) + if(ismob(pilot)) + to_chat(pilot, SPAN_NOTICE("Your mech reports that negating the EMP cost [(percent_power_used)]% charge.")) + else + for(var/pilot in pilots) + if(ismob(pilot)) + to_chat(pilot, SPAN_DANGER("Your mech's EMP countermeasures deactivate as power levels drop too low.")) + else + var/ratio = get_blocked_ratio(null, DAMAGE_BURN, null, (4-severity) * 20) + emp_damage += round((12 - (severity*3))*( 1 - ratio)) - var/ratio = get_blocked_ratio(null, DAMAGE_BURN, null, (4-severity) * 20) - - if(ratio >= 0.5) - for(var/mob/living/m in pilots) - to_chat(m, SPAN_NOTICE("Your Faraday shielding absorbed the pulse!")) - return - else if(ratio > 0) - for(var/mob/living/m in pilots) - to_chat(m, SPAN_NOTICE("Your Faraday shielding mitigated the pulse!")) - - emp_damage += round((12 - (severity*3))*( 1 - ratio)) - - for(var/obj/item/thing in list(arms,legs,head,body)) - thing.emp_act(severity) - if(!hatch_closed || !prob(body.pilot_coverage)) - for(var/thing in pilots) - var/mob/pilot = thing - pilot.emp_act(severity) + update_emp_protection() /mob/living/heavy_vehicle/fall_impact(levels_fallen, stopped_early = FALSE, var/damage_mod = 1) // No gravity, stop falling into spess! diff --git a/code/modules/heavy_vehicle/mech_helpers.dm b/code/modules/heavy_vehicle/mech_helpers.dm index 9a19ba24964..d45b4a65420 100644 --- a/code/modules/heavy_vehicle/mech_helpers.dm +++ b/code/modules/heavy_vehicle/mech_helpers.dm @@ -161,20 +161,32 @@ /mob/living/heavy_vehicle/proc/use_cell_power(var/power_to_use) var/power_used = get_cell()?.use(power_to_use) - if(power_used <= 0) + if(power_used < power_to_use - 1) //Self recharging cells would otherwise allow a mech to run indefinately at full functionality with 0.1% charge. for(var/hardpoint in hardpoints) var/obj/item/mecha_equipment/ME = hardpoints[hardpoint] if(ME) ME.deactivate() + if(power == MECH_POWER_ON) + playsound(src, 'sound/mecha/mech-shutdown.ogg', 100, 0) + power = MECH_POWER_OFF + update_emp_protection() + if(hud_power_control) + SSicon_update.add_to_queue(hud_power_control) return power_used /mob/living/heavy_vehicle/proc/drain_cell_power(var/power_to_drain) var/power_used = get_cell()?.drain_power(0, 0, power_to_drain) - if(power_used <= 0) + if(power_used < power_to_drain - 1) //Self recharging cells would otherwise allow a mech to run indefinately at full functionality with 0.1% charge. for(var/hardpoint in hardpoints) var/obj/item/mecha_equipment/ME = hardpoints[hardpoint] if(ME) ME.deactivate() + if(power == MECH_POWER_ON) + playsound(src, 'sound/mecha/mech-shutdown.ogg', 100, 0) + power = MECH_POWER_OFF + update_emp_protection() + if(hud_power_control) + SSicon_update.add_to_queue(hud_power_control) return power_used /mob/living/heavy_vehicle/proc/checked_use_cell(var/power_to_drain) diff --git a/code/modules/heavy_vehicle/mecha.dm b/code/modules/heavy_vehicle/mecha.dm index aaa75e5f4be..ba1d7a5b045 100644 --- a/code/modules/heavy_vehicle/mecha.dm +++ b/code/modules/heavy_vehicle/mecha.dm @@ -266,6 +266,12 @@ /mob/living/heavy_vehicle/return_air() return (body && body.pilot_coverage >= 100 && hatch_closed) ? body.cockpit : loc?.return_air() +/mob/living/heavy_vehicle/proc/update_emp_protection() + RemoveElement(/datum/element/empprotection, EMP_PROTECT_ALL) + if(power == MECH_POWER_ON && body.mech_armor && body.mech_armor.emp_protection) + if(get_cell().charge > 500) + AddElement(/datum/element/empprotection, EMP_PROTECT_ALL) + /mob/living/heavy_vehicle/GetIdCard() return access_card @@ -288,18 +294,22 @@ to_chat(reciever, SPAN_NOTICE("Power transition in progress. Please wait.")) else if(power == MECH_POWER_ON) //Turning it off is instant power = MECH_POWER_OFF - else if(get_cell(TRUE)) + update_emp_protection() + else if(get_cell(TRUE).check_charge(1000)) //Check if we have enough charge to power on //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) power = MECH_POWER_ON + update_emp_protection() sound_looping = TRUE soundloop.start() else to_chat(reciever, SPAN_WARNING("You abort the powerup sequence.")) power = MECH_POWER_OFF - hud_power_control?.queue_icon_update() + update_emp_protection() + if(hud_power_control) + SSicon_update.add_to_queue(hud_power_control) else to_chat(reciever, SPAN_WARNING("Error: No power cell was detected.")) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 86390aa97cb..a6f17c32188 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -56,8 +56,7 @@ //Auras, essentially magic and encompassing around us, gets checked first if(!src.aura_check(AURA_TYPE_BULLET, hitting_projectile, def_zone, piercing_hit)) blocked = 100 - return BULLET_ACT_FORCE_PIERCE - + return BULLET_ACT_BLOCK //Shields, in front of us (hopefully), check the bullet before it reaches us var/shield_check = check_shields(hitting_projectile.damage, hitting_projectile, hitting_projectile.firer, def_zone, "the [hitting_projectile.name]") diff --git a/html/changelogs/Fenodyree-MechBugfixes.yml b/html/changelogs/Fenodyree-MechBugfixes.yml new file mode 100644 index 00000000000..727e98bbe2d --- /dev/null +++ b/html/changelogs/Fenodyree-MechBugfixes.yml @@ -0,0 +1,10 @@ +author: Fenodyree +delete-after: True +changes: + - bugfix: "Fixes EMP armor for mechs." + - bugfix: "Fixes mech pilot's getting EMP'd twice for every EMP hitting the mech." + - bugfix: "Fixes shields for mechs." + - bugfix: "Fixes mechs being able to draw infinite power, as long as there was any power in their cell." + - bugfix: "Fixes cockpit coverage not letting the pilot be hit." + - bugfix: "Fixes legacy icon updates." + - rscadd: "Adds a chance for attacks to hit the mech's pilot after the cockpit has been destroyed. This still accounts for the mech's armor and shields."